-
-
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
Conversation
…ate csv files for access logs, and added tests
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.
A nitpick that I need to somehow get into our organization-wide development guidelines: SubmissionsExport…
should be SubmissionExport
. Another example of something similar would be that we use AssetSerializer
instead of AssetsSerializer
eadbd10
to
4079ea0
Compare
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.
We should figure out what is going on with the diff here (208 files changed) before we proceed with this. I'm sure whoever else looks at this will notice, but I'm marking it as change requested out of an abundance of caution.
@jamesrkiger Yes sorry I'm working on cleaning this up, something with my force push went wrong. |
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.
I think we can simplify this refactor a bit.
If we make CommonExportTask a mixin we won't need to do the difficult migration.
Also, I think instead of having a lot of methods that are like
do_thing(task):
if task is project_view:
do_stuff
elif task is export:
do other stuff
we can make the task classes themselves responsible for generating the data.
Then export_task_in_background
would look like (pseudocode)
model = get_model()
task = model.get(uid=uid)
task.run()
if status is complete:
subject = model.default_email_subject
...get the result and send the email
and in CommonExportTask, run_task
could do something like
data = self.get_data()
url = create_export_file()
return url
and AccessLogExportTask
and ProjectViewExportTask
would each have their own implementations of get_data()
and a value for default_email_subject
.
Usually inheritance is preferable to if-else chains because the code it's easier to change one subclass without changing methods that affect other subclasses.
kpi/models/import_export_task.py
Outdated
@@ -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 comment
The 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.
kpi/utils/data_exports.py
Outdated
} | ||
|
||
|
||
def create_data_export( |
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.
If we do it this way we'll have to add to this method every time we create a new kind of export task. I think it would make more sense to have this method take just take data and headers and create a csv from it. The AccessLogExportTask and ProjectViewExportTask classes would do whatever they had to do to get the data and then call this method with the data they collected.
kpi/utils/data_exports.py
Outdated
if isinstance(val, str): | ||
val = val.replace('\n', '') | ||
return val | ||
def get_data(filtered_queryset: QuerySet, export_type: str) -> QuerySet: |
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.
now that we have more cases, we should just have each type of ExportTask have its own method for getting data so we don't just keep adding elif
s for every new kind of export task.
'abstract': False, | ||
}, | ||
), | ||
migrations.RunPython(populate_common_export_tasks), |
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.
Forgot: if we end up keeping the migration, this needs to be reversible, even if the reverse is RunPython.noop. Otherwise unapplying migrations breaks
…t_email_subject to the ProjectView and AccessLog export tasks
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.
some small things
kobo/apps/audit_log/models.py
Outdated
@@ -610,7 +610,7 @@ def create_from_related_request( | |||
action = modify_action | |||
if action: | |||
# some actions on related objects do not need to be logged, | |||
# eg deleting an ExportTask | |||
# eg deleting an SubmissionExportTask |
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.
# eg deleting an SubmissionExportTask | |
# eg deleting a SubmissionExportTask |
nit, definitely non-blocking
kpi/models/import_export_task.py
Outdated
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 comment
The reason will be displayed to describe this comment to others. Learn more.
def _run_task_base(self, messages: list, buff) -> None: | |
def _export_data_to_file(self, messages: list, buff) -> None: |
since it's no longer doing the whole task
return 'Access Log Report Complete' | ||
|
||
def get_data(self, filtered_queryset: QuerySet) -> QuerySet: | ||
user_url = Concat( |
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.
didn't know you could do this outside a query. neat.
kpi/models/import_export_task.py
Outdated
return 'Report Complete' | ||
|
||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
This should account for data
not having a view
key since we don't need one for access logs exports. the filename can just be export_type-username-time
for tasks without a view.
kpi/models/import_export_task.py
Outdated
return 'Report Complete' | ||
|
||
def _get_export_details(self) -> tuple: | ||
return self.data['type'], self.data['view'] | ||
|
||
def _build_export_filename( | ||
self, export_type: str, username: str, view: str | ||
) -> str: | ||
time = timezone.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 comment
The 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 view
parameter
kpi/models/import_export_task.py
Outdated
'ip_address', | ||
'initial_user_username', | ||
'initial_user_uid', | ||
'auth_app_name', |
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.
'auth_app_name', | |
'authorized_app_name', |
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.
Just the ordering thing and then I think we're good to go!
from kobo.apps.audit_log.models import AccessLog | ||
from kpi.models.import_export_task import AccessLogExportTask | ||
|
||
User = get_user_model() |
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 (non-blocking)
If you want to avoid this boilerplate, you can use from kobo.apps.kobo_auth.shortcuts import User
|
||
self.assertIsNotNone(task.result, 'The task.result should not be None.') | ||
expected_pattern = ( | ||
rf'{self.superuser.username}/exports/access_logs_export-' |
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.
you are way more patient than I am to write this regex. respect.
@@ -79,9 +92,43 @@ | |||
'key': METADATA, | |||
'columns': USER_FIELDS + METADATA_FIELDS, | |||
}, | |||
'access_logs_export': { |
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.
We should order by '-date-created
to be consistent with the endpoint
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.
LGTM
👀 Preview steps
Export all data
button forProjectViews
still works as normal and that the email header has the appropriate title: "Project View Report Complete"Export
button is exporting data correctly./manage.py shell_plus
test_user = User.objects.get(username='test')
print(task)
task.run()
print(task.result)
💭 Notes
user
touser_url
because naming ituser
conflicts with an existing field in theAccessLog
modelproject_view_exports.py
todata_exports.py
because it now generates exports for both project view and access log exportsProjectViewExportTask
to have a baseCommonExportTask
which both the project view and access log export classes inherit fromkpi/tasks.py
) to have the correct subject title based on the type of export report it is sendingdata_exports.py