From 014b373a7878b3364863f597c93b456cc94d9fb4 Mon Sep 17 00:00:00 2001 From: "Christopher C. Wells" Date: Sun, 16 Feb 2020 15:58:49 -0800 Subject: [PATCH] Add initial basic admin import/export support --- Pipfile | 6 +- Pipfile.lock | 109 ++++++++++++++++-- babybuddy/settings/base.py | 7 ++ core/admin.py | 69 +++++++++-- .../action_formats.11c3e817b80a.js | 22 ++++ .../action_formats.11c3e817b80a.js.gz | Bin 0 -> 305 bytes static/import_export/action_formats.js | 22 ++++ static/import_export/action_formats.js.gz | Bin 0 -> 305 bytes static/import_export/import.358144dd8713.css | 81 +++++++++++++ .../import_export/import.358144dd8713.css.gz | Bin 0 -> 493 bytes static/import_export/import.css | 81 +++++++++++++ static/import_export/import.css.gz | Bin 0 -> 493 bytes static/staticfiles.json | 2 +- 13 files changed, 375 insertions(+), 24 deletions(-) create mode 100644 static/import_export/action_formats.11c3e817b80a.js create mode 100644 static/import_export/action_formats.11c3e817b80a.js.gz create mode 100644 static/import_export/action_formats.js create mode 100644 static/import_export/action_formats.js.gz create mode 100644 static/import_export/import.358144dd8713.css create mode 100644 static/import_export/import.358144dd8713.css.gz create mode 100644 static/import_export/import.css create mode 100644 static/import_export/import.css.gz diff --git a/Pipfile b/Pipfile index 0bf2d3f0..62a51bb3 100644 --- a/Pipfile +++ b/Pipfile @@ -1,11 +1,8 @@ [[source]] - verify_ssl = true url = "https://pypi.python.org/simple" - [packages] - django = "*" djangorestframework = "*" django-filter = "*" @@ -21,10 +18,9 @@ easy-thumbnails = "*" python-dotenv = "*" django-storages = "*" boto3 = "*" - +django-import-export = "*" [dev-packages] - coveralls = "*" flake8 = "*" ipaddress = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 7213b2cc..917bc111 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "21a6001d830ce191858fed3bd90d35af9317737dddfcb41a3dd1e714f8e8fd54" + "sha256": "60e32aea6660fd1030af59723675270b96317b565230d48f7262f158f82c2ee3" }, "pipfile-spec": 6, "requires": {}, @@ -22,17 +22,30 @@ }, "boto3": { "hashes": [ - "sha256:09eccb6cd41381c4ff1d626c3a19884b5b1f1424d15a96004d077b532ef393d1", - "sha256:664be6e0e20cb064dda4ac3397082e3dcc453abb8b2bd2cf64066677e0fb2266" + "sha256:33462a79d57c9c4a215e075472509537d03545f54566fc4f776fb0f4cfa616f6", + "sha256:34f9a04f529dc849f0e427782d6f3c6b62f7fb734d8f4859b17e5dee0855323e" ], - "version": "==1.11.13" + "version": "==1.12.0" }, "botocore": { "hashes": [ - "sha256:6478d9207db6dbcb5106fd4db2cdd5194d0b2dc0b73776019d56877ab802fe87", - "sha256:6ffb78b331b0954cfe5c51958cb51522ab0e2999442422949b080a3e1bc76ee1" + "sha256:055da4826f6c9158e4a61549d57a2ce449c27d44ce34ab4c96c7bb7b5c993efc", + "sha256:1f7cecfcd38c7cac17b5386014eb04626d1c7559ee8d8ec1526058cd23f6d1d4" ], - "version": "==1.14.13" + "version": "==1.15.0" + }, + "defusedxml": { + "hashes": [ + "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93", + "sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5" + ], + "version": "==0.6.0" + }, + "diff-match-patch": { + "hashes": [ + "sha256:a809a996d0f09b9bbd59e9bbd0b71eed8c807922512910e05cbd3f9480712ddb" + ], + "version": "==20181111" }, "dj-database-url": { "hashes": [ @@ -55,6 +68,13 @@ ], "version": "==2.2.0" }, + "django-import-export": { + "hashes": [ + "sha256:939109c8be31a8bcc8779634c080c4510669aba576b173f37cfdf838a705d0ac", + "sha256:94c2030ba1b141bdd6423ac479b4da79b7de55c5a9f339b675c46299db184c11" + ], + "version": "==2.0.1" + }, "django-storages": { "hashes": [ "sha256:3103991c2ee8cef8a2ff096709973ffe7106183d211a79f22cf855f33533d924", @@ -90,6 +110,13 @@ ], "version": "==2.7" }, + "et-xmlfile": { + "hashes": [ + "sha256:1c702bcfd00f9a602dffd0e004d9b4f68a65c7410963ca76b1250413e9197a6c", + "sha256:614d9722d572f6246302c4491846d2c393c199cfa4edc9af593437691683335b" + ], + "version": "==1.0.1" + }, "faker": { "hashes": [ "sha256:047d4d1791bfb3756264da670d99df13d799bb36e7d88774b1585a82d05dbaec", @@ -104,6 +131,13 @@ ], "version": "==20.0.4" }, + "jdcal": { + "hashes": [ + "sha256:1abf1305fce18b4e8aa248cf8fe0c56ce2032392bc64bbd61b5dff2a19ec8bba", + "sha256:472872e096eb8df219c23f2689fc336668bdb43d194094b5cc1707e1640acfc8" + ], + "version": "==1.4.1" + }, "jmespath": { "hashes": [ "sha256:3720a4b1bd659dd2eecad0666459b9788813e032b83e7ba58578e48254e0a0e6", @@ -111,6 +145,12 @@ ], "version": "==0.9.4" }, + "markuppy": { + "hashes": [ + "sha256:1adee2c0a542af378fe84548ff6f6b0168f3cb7f426b46961038a2bcfaad0d5f" + ], + "version": "==1.14" + }, "numpy": { "hashes": [ "sha256:1786a08236f2c92ae0e70423c45e1e62788ed33028f94ca99c4df03f5be6b3c6", @@ -137,6 +177,18 @@ ], "version": "==1.18.1" }, + "odfpy": { + "hashes": [ + "sha256:db766a6e59c5103212f3cc92ec8dd50a0f3a02790233ed0b52148b70d3c438ec" + ], + "version": "==1.4.1" + }, + "openpyxl": { + "hashes": [ + "sha256:547a9fc6aafcf44abe358b89ed4438d077e9d92e4f182c87e2dc294186dc4b64" + ], + "version": "==3.0.3" + }, "pandas": { "hashes": [ "sha256:23e177d43e4bf68950b0f8788b6a2fef2f478f4ec94883acb627b9264522a98a", @@ -248,6 +300,22 @@ ], "version": "==2019.3" }, + "pyyaml": { + "hashes": [ + "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6", + "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf", + "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5", + "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e", + "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811", + "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e", + "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d", + "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20", + "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689", + "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994", + "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615" + ], + "version": "==5.3" + }, "retrying": { "hashes": [ "sha256:08c039560a6da2fe4f2c426d0766e284d3b736e355f8dd24b37367b0bb41973b" @@ -275,6 +343,13 @@ ], "version": "==0.3.0" }, + "tablib": { + "hashes": [ + "sha256:00654241e5beee437ba544e4fa4abef70ccec3668503aa95406c1250bb660770", + "sha256:6336e7aa3f0e5894b47270a3dc639cc2e78eb823c7ccb8c4512fb408cdc18d08" + ], + "version": "==0.14.0" + }, "text-unidecode": { "hashes": [ "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", @@ -296,6 +371,20 @@ "sha256:62556265ec1011bd87113fb81b7516f52688887b7a010ee899ff1fd18fd22700" ], "version": "==5.0.1" + }, + "xlrd": { + "hashes": [ + "sha256:546eb36cee8db40c3eaa46c351e67ffee6eeb5fa2650b71bc4c758a29a1b29b2", + "sha256:e551fb498759fa3a5384a94ccd4c3c02eb7c00ea424426e212ac0c57be9dfbde" + ], + "version": "==1.2.0" + }, + "xlwt": { + "hashes": [ + "sha256:a082260524678ba48a297d922cc385f58278b8aa68741596a87de01a9c628b2e", + "sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88" + ], + "version": "==1.3.0" } }, "develop": { @@ -351,10 +440,10 @@ }, "coveralls": { "hashes": [ - "sha256:2da39aeaef986757653f0a442ba2bef22a8ec602c8bacbc69d39f468dfae12ec", - "sha256:906e07a12b2ac04b8ad782d06173975fe5ff815fe9df3bfedd2c099bc5791aec" + "sha256:4b6bfc2a2a77b890f556bc631e35ba1ac21193c356393b66c84465c06218e135", + "sha256:67188c7ec630c5f708c31552f2bcdac4580e172219897c4136504f14b823132f" ], - "version": "==1.10.0" + "version": "==1.11.1" }, "docopt": { "hashes": [ diff --git a/babybuddy/settings/base.py b/babybuddy/settings/base.py index 968eca5b..4be89ef3 100644 --- a/babybuddy/settings/base.py +++ b/babybuddy/settings/base.py @@ -40,6 +40,7 @@ INSTALLED_APPS = [ 'widget_tweaks', 'easy_thumbnails', 'storages', + 'import_export', 'django.contrib.admin', 'django.contrib.auth', @@ -215,6 +216,12 @@ REST_FRAMEWORK = { 'PAGE_SIZE': 100 } +# Import/Export configuration +# See https://django-import-export.readthedocs.io/ + +IMPORT_EXPORT_IMPORT_PERMISSION_CODE = 'add' +IMPORT_EXPORT_EXPORT_PERMISSION_CODE = 'change' + # Baby Buddy configuration # See README.md#configuration for details about these settings. diff --git a/core/admin.py b/core/admin.py index 15723d45..06650e50 100644 --- a/core/admin.py +++ b/core/admin.py @@ -2,54 +2,95 @@ from django.contrib import admin from django.conf import settings +from import_export import resources +from import_export.admin import ImportExportMixin + from core import models +class ChildImportExportResource(resources.ModelResource): + class Meta: + model = models.Child + exclude = ('picture', 'slug') + + @admin.register(models.Child) -class ChildAdmin(admin.ModelAdmin): +class ChildAdmin(ImportExportMixin, admin.ModelAdmin): list_display = ('first_name', 'last_name', 'birth_date', 'slug') list_filter = ('last_name',) search_fields = ('first_name', 'last_name', 'birth_date') fields = ['first_name', 'last_name', 'birth_date'] if settings.BABY_BUDDY['ALLOW_UPLOADS']: fields.append('picture') + resource_class = ChildImportExportResource + + +class DiaperChangeImportExportResource(resources.ModelResource): + class Meta: + model = models.DiaperChange @admin.register(models.DiaperChange) -class DiaperChangeAdmin(admin.ModelAdmin): +class DiaperChangeAdmin(ImportExportMixin, admin.ModelAdmin): list_display = ('child', 'time', 'wet', 'solid', 'color') list_filter = ('child', 'wet', 'solid', 'color') search_fields = ('child__first_name', 'child__last_name',) + resource_class = DiaperChangeImportExportResource + + +class FeedingImportExportResource(resources.ModelResource): + class Meta: + model = models.Feeding @admin.register(models.Feeding) -class FeedingAdmin(admin.ModelAdmin): +class FeedingAdmin(ImportExportMixin, admin.ModelAdmin): list_display = ('start', 'end', 'duration', 'child', 'type', 'method', 'amount') list_filter = ('child', 'type', 'method',) search_fields = ('child__first_name', 'child__last_name', 'type', 'method',) + resource_class = FeedingImportExportResource + + +class NoteImportExportResource(resources.ModelResource): + class Meta: + model = models.Note @admin.register(models.Note) -class NoteAdmin(admin.ModelAdmin): +class NoteAdmin(ImportExportMixin, admin.ModelAdmin): list_display = ('time', 'child', 'note',) list_filter = ('child',) search_fields = ('child__last_name',) + resource_class = NoteImportExportResource + + +class SleepImportExportResource(resources.ModelResource): + class Meta: + model = models.Sleep + exclude = ('duration',) @admin.register(models.Sleep) -class SleepAdmin(admin.ModelAdmin): +class SleepAdmin(ImportExportMixin, admin.ModelAdmin): list_display = ('start', 'end', 'duration', 'child', 'nap') list_filter = ('child',) search_fields = ('child__first_name', 'child__last_name',) + resource_class = SleepImportExportResource + + +class TemperatureImportExportResource(resources.ModelResource): + class Meta: + model = models.Temperature @admin.register(models.Temperature) -class TemperatureAdmin(admin.ModelAdmin): +class TemperatureAdmin(ImportExportMixin, admin.ModelAdmin): list_display = ('child', 'temperature', 'time',) list_filter = ('child',) search_fields = ('child__first_name', 'child__last_name', 'temperature',) + resource_class = TemperatureImportExportResource @admin.register(models.Timer) @@ -60,15 +101,27 @@ class TimerAdmin(admin.ModelAdmin): search_fields = ('child__first_name', 'child__last_name', 'name', 'user') +class TummyTimeImportExportResource(resources.ModelResource): + class Meta: + model = models.TummyTime + + @admin.register(models.TummyTime) -class TummyTimeAdmin(admin.ModelAdmin): +class TummyTimeAdmin(ImportExportMixin, admin.ModelAdmin): list_display = ('start', 'end', 'duration', 'child', 'milestone',) list_filter = ('child',) search_fields = ('child__first_name', 'child__last_name', 'milestone',) + resource_class = TummyTimeImportExportResource + + +class WeightImportExportResource(resources.ModelResource): + class Meta: + model = models.Weight @admin.register(models.Weight) -class WeightAdmin(admin.ModelAdmin): +class WeightAdmin(ImportExportMixin, admin.ModelAdmin): list_display = ('child', 'weight', 'date',) list_filter = ('child',) search_fields = ('child__first_name', 'child__last_name', 'weight',) + resource_class = WeightImportExportResource diff --git a/static/import_export/action_formats.11c3e817b80a.js b/static/import_export/action_formats.11c3e817b80a.js new file mode 100644 index 00000000..9f0fe571 --- /dev/null +++ b/static/import_export/action_formats.11c3e817b80a.js @@ -0,0 +1,22 @@ +(function($) { + $(document).ready(function() { + var $actionsSelect, $formatsElement; + if ($('body').hasClass('grp-change-list')) { + // using grappelli + $actionsSelect = $('#grp-changelist-form select[name="action"]'); + $formatsElement = $('#grp-changelist-form select[name="file_format"]'); + } else { + // using default admin + $actionsSelect = $('#changelist-form select[name="action"]'); + $formatsElement = $('#changelist-form select[name="file_format"]').parent(); + } + $actionsSelect.change(function() { + if ($(this).val() === 'export_admin_action') { + $formatsElement.show(); + } else { + $formatsElement.hide(); + } + }); + $actionsSelect.change(); + }); +})(django.jQuery); diff --git a/static/import_export/action_formats.11c3e817b80a.js.gz b/static/import_export/action_formats.11c3e817b80a.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..d5a48a3fb661c1c6f5f3a8a9a0d8485ce2eaebff GIT binary patch literal 305 zcmV-10nYv(iwFP!00002|E-eUO2jY_h41$%hP99cZ1w?`UWpGNUW$~lnYLNdq$KHT zk$rdjhT6iWy zMZAZkbiTnD9u1AN>q)3f;0b4MPW4R48pDIZ5IC!Rn-(?Nic+IPWMWSq*J=e_(6)lg zW80E3`p`N*6tclAyF0>0Ryz8CFbQ5PHk2*rq2((R>m%vh?r%}0Hk8k!N8S#|1iF<# zQHh<2fU42<=M?{x;4kiFi+-5o`xuLBb8_Zge}Vnm###p&y>jL{Q>|0 D7EhU$ literal 0 HcmV?d00001 diff --git a/static/import_export/action_formats.js b/static/import_export/action_formats.js new file mode 100644 index 00000000..9f0fe571 --- /dev/null +++ b/static/import_export/action_formats.js @@ -0,0 +1,22 @@ +(function($) { + $(document).ready(function() { + var $actionsSelect, $formatsElement; + if ($('body').hasClass('grp-change-list')) { + // using grappelli + $actionsSelect = $('#grp-changelist-form select[name="action"]'); + $formatsElement = $('#grp-changelist-form select[name="file_format"]'); + } else { + // using default admin + $actionsSelect = $('#changelist-form select[name="action"]'); + $formatsElement = $('#changelist-form select[name="file_format"]').parent(); + } + $actionsSelect.change(function() { + if ($(this).val() === 'export_admin_action') { + $formatsElement.show(); + } else { + $formatsElement.hide(); + } + }); + $actionsSelect.change(); + }); +})(django.jQuery); diff --git a/static/import_export/action_formats.js.gz b/static/import_export/action_formats.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..d5a48a3fb661c1c6f5f3a8a9a0d8485ce2eaebff GIT binary patch literal 305 zcmV-10nYv(iwFP!00002|E-eUO2jY_h41$%hP99cZ1w?`UWpGNUW$~lnYLNdq$KHT zk$rdjhT6iWy zMZAZkbiTnD9u1AN>q)3f;0b4MPW4R48pDIZ5IC!Rn-(?Nic+IPWMWSq*J=e_(6)lg zW80E3`p`N*6tclAyF0>0Ryz8CFbQ5PHk2*rq2((R>m%vh?r%}0Hk8k!N8S#|1iF<# zQHh<2fU42<=M?{x;4kiFi+-5o`xuLBb8_Zge}Vnm###p&y>jL{Q>|0 D7EhU$ literal 0 HcmV?d00001 diff --git a/static/import_export/import.358144dd8713.css b/static/import_export/import.358144dd8713.css new file mode 100644 index 00000000..bb20ba2a --- /dev/null +++ b/static/import_export/import.358144dd8713.css @@ -0,0 +1,81 @@ +.import-preview .errors { + position: relative; +} + +.validation-error-count { + display: inline-block; + background-color: #e40000; + border-radius: 6px; + color: white; + font-size: 0.9em; + position: relative; + font-weight: bold; + margin-top: -2px; + padding: 0.2em 0.4em; +} + +.validation-error-container { + position: absolute; + opacity: 0; + pointer-events: none; + background-color: #ffc1c1; + padding: 14px 15px 10px; + top: 25px; + margin: 0 0 20px 0; + width: 200px; + z-index: 2; +} + +table.import-preview tr.skip { + background-color: #d2d2d2; +} + +table.import-preview tr.new { + background-color: #bdd8b2; +} + +table.import-preview tr.delete { + background-color: #f9bebf; +} + +table.import-preview tr.update { + background-color: #fdfdcf; +} + +.import-preview td:hover .validation-error-count { + z-index: 3; +} +.import-preview td:hover .validation-error-container { + opacity: 1; + pointer-events: auto; +} + +.validation-error-list { + margin: 0; + padding: 0; +} + +.validation-error-list li { + list-style: none; + margin: 0; +} + +.validation-error-list > li > ul { + margin: 8px 0; + padding: 0; +} + +.validation-error-list > li > ul > li { + padding: 0; + margin: 0 0 10px; + line-height: 1.28em; +} + +.validation-error-field-label { + display: block; + border-bottom: 1px solid #e40000; + color: #e40000; + text-transform: uppercase; + font-weight: bold; + font-size: 0.85em; +} diff --git a/static/import_export/import.358144dd8713.css.gz b/static/import_export/import.358144dd8713.css.gz new file mode 100644 index 0000000000000000000000000000000000000000..dce505a140246e96fd64686fe8482289d099d0ef GIT binary patch literal 493 zcmVl>TfLDO{$zDgm4xGgy2FHzZXJy7m?7}Mg%a5@C2L1pT(j$DT7+2 z897GF&JC7Eh{ouRdT#_;gBE0KTzA;;M60^PE^wUYXyZa7e!#NiXPk6FLy$pf9AYDW z`AZtT#+~;_kovu|BqM%6Bg*0pj++^Y*T^&AZch!98=VG^D(uioa=sC=&P#ZuHQHSw ztl`M-Wul$Q(URhn!l$iKZFFYH3Y}LS5@%m#ZD@(pf)iNcw5+o*vlV^cRb4evR4x5Q zRIBtVv)EC2y~>@O4X?zl=77B988z)WsJucxBw7s@9vQPD)fy;1oj8Re9?*|zeIrfR zsl9|-{>-3Tt=H{qv<3qNa|Hcc3vK_BVDNlU^8~urT_*T~)y>|W_(Weh`o4uf6UTp1 zJR9b2jp{pVsDa#!?+nH)dUADX9g#aS8_VY_i~6|&eXU6Z j7m^f|jlByD8@z|msW@L?CiQ8(%8LI2AGSr{U li > ul { + margin: 8px 0; + padding: 0; +} + +.validation-error-list > li > ul > li { + padding: 0; + margin: 0 0 10px; + line-height: 1.28em; +} + +.validation-error-field-label { + display: block; + border-bottom: 1px solid #e40000; + color: #e40000; + text-transform: uppercase; + font-weight: bold; + font-size: 0.85em; +} diff --git a/static/import_export/import.css.gz b/static/import_export/import.css.gz new file mode 100644 index 0000000000000000000000000000000000000000..dce505a140246e96fd64686fe8482289d099d0ef GIT binary patch literal 493 zcmVl>TfLDO{$zDgm4xGgy2FHzZXJy7m?7}Mg%a5@C2L1pT(j$DT7+2 z897GF&JC7Eh{ouRdT#_;gBE0KTzA;;M60^PE^wUYXyZa7e!#NiXPk6FLy$pf9AYDW z`AZtT#+~;_kovu|BqM%6Bg*0pj++^Y*T^&AZch!98=VG^D(uioa=sC=&P#ZuHQHSw ztl`M-Wul$Q(URhn!l$iKZFFYH3Y}LS5@%m#ZD@(pf)iNcw5+o*vlV^cRb4evR4x5Q zRIBtVv)EC2y~>@O4X?zl=77B988z)WsJucxBw7s@9vQPD)fy;1oj8Re9?*|zeIrfR zsl9|-{>-3Tt=H{qv<3qNa|Hcc3vK_BVDNlU^8~urT_*T~)y>|W_(Weh`o4uf6UTp1 zJR9b2jp{pVsDa#!?+nH)dUADX9g#aS8_VY_i~6|&eXU6Z j7m^f|jlByD8@z|msW@L?CiQ8(%8LI2AGSr{U