-
Notifications
You must be signed in to change notification settings - Fork 0
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
Add import and export functionality for App Users. #4
Changes from 4 commits
8fdc483
614593d
33901b5
2456a92
0299e1c
e514933
44d597b
c6775e1
81ab67e
408befd
5edac67
99c306d
2841f51
4d8f8c5
3a07ede
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 | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -21,4 +21,12 @@ jobs: | |||||||||||||
sudo apt-get install -y --no-install-recommends postgresql-client | ||||||||||||||
pip install -U -q pip-tools | ||||||||||||||
pip-sync requirements/base/base.txt requirements/dev/dev.txt | ||||||||||||||
- run: pytest | ||||||||||||||
- name: Bring up auxiliary services | ||||||||||||||
run: | | ||||||||||||||
export COMPOSE_PARALLEL_LIMIT=1 | ||||||||||||||
while ! docker compose pull; do sleep 1; done | ||||||||||||||
docker compose up --detach | ||||||||||||||
- name: Run tests | ||||||||||||||
run: | | ||||||||||||||
export DATABASE_URL=postgresql://postgres@localhost:9062/odk_publish | ||||||||||||||
pytest | ||||||||||||||
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
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
from decimal import InvalidOperation | ||
from functools import partial | ||
|
||
from django.core.exceptions import ValidationError | ||
from import_export import resources, fields, widgets | ||
|
||
from .models import AppUser, AppUserTemplateVariable | ||
|
||
|
||
class AppUserTemplateVariableWidget(widgets.ForeignKeyWidget): | ||
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 think it could be helpful to add comments to the classes/methods in this file, to help provide context of when methods are called. |
||
def __init__(self, template_variable, **kwargs): | ||
self.template_variable = template_variable | ||
super().__init__(AppUserTemplateVariable, field="value", **kwargs) | ||
|
||
def get_lookup_kwargs(self, value, row, **kwargs): | ||
return {"template_variable": self.template_variable, "app_user_id": row["id"] or None} | ||
|
||
def clean(self, value, row=None, **kwargs): | ||
try: | ||
template_variable = super().clean(value, row, **kwargs) | ||
except AppUserTemplateVariable.DoesNotExist: | ||
template_variable = AppUserTemplateVariable( | ||
value=value, **self.get_lookup_kwargs(value, row, **kwargs) | ||
) | ||
if template_variable is not None: | ||
template_variable.value = value | ||
try: | ||
template_variable.full_clean(exclude=["app_user"]) | ||
except ValidationError as e: | ||
raise ValueError(e.messages[0]) | ||
return template_variable | ||
|
||
|
||
class AppUserTemplateVariableField(fields.Field): | ||
def save(self, instance, row, is_m2m=False, **kwargs): | ||
cleaned = self.clean(row, **kwargs) | ||
if cleaned is None: | ||
instance._template_variables_to_delete.append(self.column_name) | ||
else: | ||
instance._template_variables_to_save.append(cleaned) | ||
|
||
|
||
class PositiveIntegerWidget(widgets.IntegerWidget): | ||
def clean(self, value, row=None, **kwargs): | ||
try: | ||
val = super().clean(value, row, **kwargs) | ||
except InvalidOperation: | ||
raise ValueError("Value must be an integer.") | ||
if val and val < 0: | ||
raise ValueError("Value must be positive.") | ||
return val | ||
|
||
|
||
class AppUserResource(resources.ModelResource): | ||
central_id = fields.Field( | ||
attribute="central_id", column_name="central_id", widget=PositiveIntegerWidget() | ||
) | ||
|
||
class Meta: | ||
model = AppUser | ||
fields = ("id", "name", "central_id") | ||
clean_model_instances = True | ||
|
||
def __init__(self, project): | ||
self.project = project | ||
super().__init__() | ||
for template_variable in project.template_variables.all(): | ||
self.fields[template_variable.name] = AppUserTemplateVariableField( | ||
attribute=template_variable.name, | ||
column_name=template_variable.name, | ||
dehydrate_method=partial(self.get_template_variable_value, template_variable.pk), | ||
widget=AppUserTemplateVariableWidget(template_variable), | ||
) | ||
|
||
def export_resource(self, instance, selected_fields=None, **kwargs): | ||
instance.template_variables_dict = { | ||
i.template_variable_id: i.value for i in instance.app_user_template_variables.all() | ||
} | ||
return super().export_resource(instance, selected_fields, **kwargs) | ||
|
||
def get_queryset(self): | ||
return self.project.app_users.all() | ||
|
||
def get_instance(self, instance_loader, row): | ||
instance = super().get_instance(instance_loader, row) | ||
if row["id"] and not instance: | ||
raise ValidationError( | ||
{"id": f"An app user with ID {row['id']} does not exist in the current project."} | ||
) | ||
return instance | ||
|
||
@staticmethod | ||
def get_template_variable_value(variable_pk, app_user): | ||
return app_user.template_variables_dict.get(variable_pk) | ||
|
||
def import_instance(self, instance, row, **kwargs): | ||
instance.project = self.project | ||
instance._template_variables_to_save = [] | ||
instance._template_variables_to_delete = [] | ||
super().import_instance(instance, row, **kwargs) | ||
|
||
def do_instance_save(self, instance, is_create): | ||
instance.save() | ||
for template_variable in instance._template_variables_to_save: | ||
template_variable.app_user = instance | ||
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. Maybe we could add logging here and in the delete loop below to log when certain actions occur. |
||
template_variable.save() | ||
for name in instance._template_variables_to_delete: | ||
instance.app_user_template_variables.filter(template_variable__name=name).delete() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{% extends "base.html" %} | ||
{% block content %} | ||
<div class="mb-4 grid gap-4 sm:grid-cols-2 md:mb-8 lg:grid-cols-3 xl:grid-cols-4"> | ||
{{ form.media }} | ||
<form method="post" class="max-w-2xl"> | ||
{% csrf_token %} | ||
<div class="grid gap-4 sm:grid-cols-1 sm:gap-6"> | ||
<div id="id_format_container"> | ||
{{ form.format.label_tag }} | ||
{{ form.format }} | ||
{{ form.format.errors }} | ||
</div> | ||
<div class="flex items-center space-x-4"> | ||
<button type="submit" class="btn btn-outline btn-primary">Submit</button> | ||
<a type="button" | ||
class="btn btn-outline" | ||
href="{% url 'odk_publish:app-user-list' request.odk_project.pk %}">Cancel</a> | ||
</div> | ||
</div> | ||
</form> | ||
</div> | ||
{% endblock content %} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
{% extends "base.html" %} | ||
{% block content %} | ||
<div class="mb-4 grid gap-4 sm:grid-cols-2 md:mb-8 lg:grid-cols-3 xl:grid-cols-4"> | ||
{{ form.media }} | ||
<form method="post" | ||
class="max-w-2xl" | ||
enctype="multipart/form-data" | ||
x-data="{ submitting: false }" | ||
x-on:submit="submitting = true"> | ||
{% csrf_token %} | ||
<div class="grid gap-4 sm:grid-cols-1 sm:gap-6"> | ||
{% for field in form.visible_fields %} | ||
<div id="id_import_file_container"> | ||
{{ field.label_tag }} | ||
{{ field }} | ||
{{ field.errors }} | ||
</div> | ||
{% endfor %} | ||
<div class="flex items-center space-x-4"> | ||
<button type="submit" | ||
:disabled="submitting" | ||
:class="{'btn-disabled': submitting}" | ||
class="btn btn-outline btn-primary"> | ||
Submit | ||
<svg aria-hidden="true" | ||
role="status" | ||
x-cloak | ||
x-show="submitting" | ||
class="inline-flex items-center w-4.5 h-4.5 ml-1.5 -mr-1 -mt-0.5 me-3 text-gray-200 animate-spin dark:text-gray-600" | ||
viewBox="0 0 100 101" | ||
fill="none" | ||
xmlns="http://www.w3.org/2000/svg"> | ||
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor" /> | ||
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="#1C64F2" /> | ||
</svg> | ||
</button> | ||
<a type="button" | ||
class="btn btn-outline" | ||
href="{% url 'odk_publish:app-user-list' request.odk_project.pk %}" | ||
:disabled="submitting">Cancel</a> | ||
</div> | ||
</div> | ||
</form> | ||
</div> | ||
{% endblock content %} |
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 recommend using a service for this:
Here's an example.