diff --git a/.dockerignore b/.dockerignore index 2ab5fcb4d..5fbb14ac7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,7 @@ .db/ -deploy/ \ No newline at end of file +deploy/ +.mypy_cache/ +.pytest_cache/ +go-logs/ +media/ +go.log diff --git a/.github/workflows/add-issue-to-backlog.yml b/.github/workflows/add-issue-to-backlog.yml new file mode 100644 index 000000000..0a7a4ea7f --- /dev/null +++ b/.github/workflows/add-issue-to-backlog.yml @@ -0,0 +1,16 @@ +name: Add issues to Backlog + +on: + issues: + types: + - opened + +jobs: + add-to-project: + name: Add issue to project + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v0.4.0 + with: + project-url: https://github.com/orgs/IFRCGo/projects/12 + github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ea774249..3df3800a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,79 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## Unreleased +## 1.1.471 + +### Added + - Hotfix for DREF permissions + +## 1.1.470 + +### Added + - Internal plan files: collecting only PDF-s + - Nicer LocalUnit admin lists + - ISO3 filtering possibility for districts and appeals + - GitHub Actions - add issue to Backlog project + +## 1.1.469 + +### Added + - Ingest country plan and internal plan files + - Bump up cryptography and django modules + - Surge Alert statuses: Open, Closed, Stood down + +## 1.1.468 + +### Added + - Fix Surge Alert error 500 when no linked event + +## 1.1.467 + +### Added + - Pagination fix (event, personnel) + +## 1.1.466 + +### Added + - DREF Final Report finalization + new features + - LocalUnits + - Event visibility fix + +## 1.1.465 + +### Added + - Ingest country plan + +## 1.1.464 + +### Added + - Fix timeout in Surgealert: Export All + - Bump up some packages + - Fix some settings + +## 1.1.463 + +### Added + - DREF Final report, v0.1 + - Ops Update: optimistic lock + - Dropping non-used enum Choices + - Fix filter in DREF + - Admin page: search possibility of Admin2 countries + +## 1.1.462 + +### Added + - Country Plans – strategic priorities + - Adding IDN, MYS, PHL, POL to Admin2 areas + +## 1.1.461 + +### Added + - Introducing COUNTRY PLANs + - Only active users to be shown in DREF forms (for sharing) + - Add centroid processing for Admin2 + - DREF Ops Update validation fixes + - Update snapshottest to 0.6.0 (and other small modules) + ## 1.1.460 ### Added @@ -2090,7 +2163,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## 0.1.20 -[Unreleased]: https://github.com/IFRCGo/go-api/compare/1.1.460...HEAD +[Unreleased]: https://github.com/IFRCGo/go-api/compare/1.1.471...HEAD +[1.1.471]: https://github.com/IFRCGo/go-api/compare/1.1.470...1.1.471 +[1.1.470]: https://github.com/IFRCGo/go-api/compare/1.1.469...1.1.470 +[1.1.469]: https://github.com/IFRCGo/go-api/compare/1.1.468...1.1.469 +[1.1.468]: https://github.com/IFRCGo/go-api/compare/1.1.467...1.1.468 +[1.1.467]: https://github.com/IFRCGo/go-api/compare/1.1.466...1.1.467 +[1.1.466]: https://github.com/IFRCGo/go-api/compare/1.1.465...1.1.466 +[1.1.465]: https://github.com/IFRCGo/go-api/compare/1.1.464...1.1.465 +[1.1.464]: https://github.com/IFRCGo/go-api/compare/1.1.463...1.1.464 +[1.1.463]: https://github.com/IFRCGo/go-api/compare/1.1.462...1.1.463 +[1.1.462]: https://github.com/IFRCGo/go-api/compare/1.1.461...1.1.462 +[1.1.461]: https://github.com/IFRCGo/go-api/compare/1.1.460...1.1.461 [1.1.460]: https://github.com/IFRCGo/go-api/compare/1.1.459...1.1.460 [1.1.459]: https://github.com/IFRCGo/go-api/compare/1.1.458...1.1.459 [1.1.458]: https://github.com/IFRCGo/go-api/compare/1.1.457...1.1.458 diff --git a/README.md b/README.md index 2f3534061..3d16b0e82 100644 --- a/README.md +++ b/README.md @@ -177,13 +177,27 @@ set. Check the script for the specific variables in your environment. ## Deployment command ```(bash) -docker run -p 80:80 --env-file .env -d -t ifrcgo/go-api:{TAG_NUMBER} +docker-compose up serve celery +``` +or (just the base serve command): +```(bash) +docker-compose run --rm --service-ports serve ``` - ## Comment for loading data In `main/runserver.sh` the line containing the `loaddata` command is only necessary when creating a new database. In other cases it might be causing the conflict, so it is commented. +## Initializing ElasticSearch + +For the initial creation of an index +```(bash) +docker-compose exec serve bash python manage.py rebuild_index +``` +For updating the index +```(bash) +docker-compose exec serve bash python manage.py update_index +``` + # Management commands to update and import admin0 and admin1 data There are two Django management commands that helps to work with ICRC admin0 and admin1 shapefiles. These commands should be used only when you want to update geometries, or import new ones from a shapefile. The structure of the shapefile is not very flexible, but can be adjusted easily in the scripts. @@ -225,7 +239,7 @@ The shapefile should have the following mandatory fields: * admin1_id (this is the ID of the GO district this admin2 belongs to) See [this ticket](https://github.com/IFRCGo/go-api/issues/1492#issuecomment-1284120696) for a full workflow of preparing the admin2 shapefiles. -The above command will generate a list of missing admin2s in the database based on the code (we use pcodes) to a file called `missing-admin2.txt` +The above command will generate a list of missing admin2-s in the database based on the code (we use pcodes) to a file called `missing-admin2.txt` ### Options available for the command * `--update-geom` -- updates the geometry for all admin2 matched in the shapefile. @@ -237,13 +251,13 @@ The above command will generate a list of missing admin2s in the database based Run `python manage.py update-region-bbox` to update the bbox for each region in the database. ## Import FDRS codes -Run `python manage.py import-fdrs iso-fdrs.csv` to update the countries table with FDRS codes. The csv should have `iso,fdrs` structure +Run `python manage.py import-fdrs iso-fdrs.csv` to update the countries table with FDRS codes. The csv should have `iso, fdrs` structure ## Update sovereign state and disputed status -Run ` python manage.py update-sovereign-and-disputed new_fields.csv` to update the countries table with sovereign states and disputed status. The CSV should have the `id,iso,name,sovereign_state,disputed` columns. The matching is based on iso and name. If iso is null, we fallback to name. +Run ` python manage.py update-sovereign-and-disputed new_fields.csv` to update the countries table with sovereign states and disputed status. The CSV should have the `id,iso,name,sovereign_state,disputed` columns. The matching is based on iso and name. If iso is null, we fall back to name. ## Update Mapbox Tilesets -To update GO countries and districts Mapbox tilesets, run the management command `python manage.py update-mapbox-tilesets`. This will export all country and district geometries to a GeoJSON file, and then upload them to Mapbox. The tilesets will take a while to process. The updated status can be viewed on the Mapbox Studio under tilesets. To run this management command, MAPBOX_ACCESS_TOKEN should be set in the environment. +To update GO countries and districts Mapbox tilesets, run the management command `python manage.py update-mapbox-tilesets`. This will export all country and district geometries to a GeoJSON file, and then upload them to Mapbox. The tilesets will take a while to process. The updated status can be viewed on the Mapbox Studio under tilesets. To run this management command, MAPBOX_ACCESS_TOKEN should be set in the environment. The referred files are in ./mapbox/..., so you should **not** run this command from an arbitrary point of the vm's filesystem (e.g. from the location of shapefiles), but from Django root. ### Options available for the command * `--production` — update production tilesets. If this flag is not set, by default the script will only update staging tiles diff --git a/api/admin.py b/api/admin.py index 614f78be4..c5c7e77ed 100644 --- a/api/admin.py +++ b/api/admin.py @@ -245,24 +245,6 @@ def field_reports(self, instance): return mark_safe('No related field reports') field_reports.short_description = 'Field Reports' -# For multiple document fields inline. TO be FIXED: only the last one is saved. Change also tabular.html (DELETEME) -# def save_formset(self, request, form, formset, change): -# if hasattr(formset.model, 'document'): # SituationReports (or other similars) -# instances = formset.save(commit=False) -# for inst in formset.deleted_objects: -# inst.delete() -# for inst in formset.changed_objects: -# inst.save() -# for inst in formset.new_objects: -# for i,one_document in enumerate(request.FILES.getlist('documents_multiple')): -# if i<30: # not letting tons of documents to be attached -# inst.name = inst.name if i == 0 else inst.name + '-' + str(i) -# inst.document = one_document -# inst.save() -# formset.save_m2m() -# else: -# formset.save() - class GdacsAdmin(CompareVersionAdmin, RegionRestrictedAdmin, TranslationAdmin): country_in = 'countries__pk__in' @@ -526,7 +508,7 @@ class DistrictAdmin(geoadmin.OSMGeoAdmin, CompareVersionAdmin, RegionRestrictedA class CountryAdmin(geoadmin.OSMGeoAdmin, CompareVersionAdmin, RegionRestrictedAdmin, TranslationAdmin): country_in = 'pk__in' - list_display = ('__str__', 'record_type') + list_display = ('__str__', 'record_type', 'iso3') region_in = 'region__pk__in' list_editable = ('record_type',) search_fields = ('name',) @@ -550,10 +532,12 @@ class RegionAdmin(geoadmin.OSMGeoAdmin, CompareVersionAdmin, RegionRestrictedAdm search_fields = ('name',) modifiable = True + class Admin2Admin(geoadmin.OSMGeoAdmin, CompareVersionAdmin, RegionRestrictedAdmin): - search_fields = ('name',) + search_fields = ('name', 'admin1__country__name') modifiable = True + class UserProfileAdmin(CompareVersionAdmin): search_fields = ('user__username', 'user__email', 'country__name',) list_filter = ( diff --git a/api/drf_views.py b/api/drf_views.py index 5b85227d6..5b9d885c0 100644 --- a/api/drf_views.py +++ b/api/drf_views.py @@ -11,13 +11,14 @@ from django_filters import rest_framework as filters from django.contrib.auth.models import User from django.db import models -from django.db.models import Prefetch, Count, Q +from django.db.models import Prefetch, Count, Q, OuterRef from django.utils import timezone from main.utils import is_tableau from deployments.models import Personnel from databank.serializers import CountryOverviewSerializer +from .utils import is_user_ifrc from .event_sources import SOURCES from .exceptions import BadRequest from .view_filters import ListFilter @@ -61,6 +62,8 @@ CountryOfFieldReportToReview, ) +from country_plan.models import CountryPlan + from .serializers import ( ActionSerializer, DisasterTypeSerializer, @@ -134,8 +137,8 @@ class DeploymentsByEventViewset(viewsets.ReadOnlyModelViewSet): 'personneldeployment__personnel', filter=Q( personneldeployment__personnel__type=Personnel.TypeChoices.RR, - personneldeployment__personnel__start_date__lte=timezone.now(), - personneldeployment__personnel__end_date__gte=timezone.now(), + personneldeployment__personnel__start_date__date__lte=timezone.now(), + personneldeployment__personnel__end_date__date__gte=timezone.now(), personneldeployment__personnel__is_active=True ) ) @@ -167,7 +170,11 @@ class DisasterTypeViewset(viewsets.ReadOnlyModelViewSet): class RegionViewset(viewsets.ReadOnlyModelViewSet): - queryset = Region.objects.all() + queryset = Region.objects.annotate( + country_plan_count=Count( + 'country__country_plan', filter=Q(country__country_plan__is_publish=True) + ) + ) def get_serializer_class(self): if self.action == 'list': @@ -185,17 +192,20 @@ class Meta: class CountryViewset(viewsets.ReadOnlyModelViewSet): - queryset = Country.objects.filter(is_deprecated=False) + queryset = Country.objects.filter(is_deprecated=False).annotate( + has_country_plan=models.Exists(CountryPlan.objects.filter(country=OuterRef('pk'), is_publish=True)) + ) filterset_class = CountryFilter search_fields = ('name',) # for /docs def get_object(self): pk = self.kwargs['pk'] + qs = self.get_queryset() try: - return Country.objects.get(pk=int(pk)) + return qs.get(pk=int(pk)) except ValueError: # NOTE: If pk is not integer try searching for name or iso - country = Country.objects.filter( + country = qs.filter( models.Q(name__iexact=str(pk)) | models.Q(iso__iexact=str(pk)) ) if country.exists(): @@ -328,7 +338,7 @@ def get_serializer_class(self): class DistrictFilter(filters.FilterSet): class Meta: model = District - fields = ('country',) + fields = ('country', 'country__iso3', 'name',) class DistrictViewset(viewsets.ReadOnlyModelViewSet): @@ -428,10 +438,20 @@ def retrieve(self, request, pk=None, *args, **kwargs): if pk: try: if self.request.user.is_authenticated: - if self.request.user.is_superuser: + if is_user_ifrc(self.request.user): instance = Event.objects.get(pk=pk) else: - instance = Event.objects.exclude(visibility=VisibilityChoices.IFRC).exclude(Q(visibility=VisibilityChoices.IFRC_NS) & ~Q(countries__id__in=UserCountry.objects.filter(user=self.request.user.id).values_list('country',flat=True).union(Profile.objects.filter(user=self.request.user.id).values_list('country',flat=True)))).get(pk=pk) + user_countries = UserCountry.objects\ + .filter(user=request.user.id).values('country')\ + .union( + Profile.objects.filter(user=request.user.id).values('country') + ) + instance = Event.objects\ + .exclude( + visibility=VisibilityChoices.IFRC)\ + .exclude( + Q(visibility=VisibilityChoices.IFRC_NS) & ~Q(countries__id__in=user_countries))\ + .get(pk=pk) else: instance = Event.objects.filter(visibility=VisibilityChoices.PUBLIC).get(pk=pk) # instance = Event.get_for(request.user).get(pk=pk) @@ -553,6 +573,7 @@ class Meta: 'valid_from': ('exact', 'gt', 'gte', 'lt', 'lte'), 'valid_to': ('exact', 'gt', 'gte', 'lt', 'lte'), 'appeal__real_data_update': ('exact', 'gt', 'gte', 'lt', 'lte'), + 'country__iso3': ('exact',), } @@ -1074,6 +1095,7 @@ class GoHistoricalViewSet(viewsets.ReadOnlyModelViewSet): def get_queryset(self): return Event.objects.filter(appeals__isnull=False) + class CountryOfFieldReportToReviewViewset(viewsets.ReadOnlyModelViewSet): queryset = CountryOfFieldReportToReview.objects.order_by('country') serializer_class = CountryOfFieldReportToReviewSerializer diff --git a/api/management/commands/index_and_notify.py b/api/management/commands/index_and_notify.py index b7d4a4694..1a7920f3b 100644 --- a/api/management/commands/index_and_notify.py +++ b/api/management/commands/index_and_notify.py @@ -479,11 +479,11 @@ def construct_template_record(self, rtype, record): 'gov_assistance': 'Yes' if record.request_assistance else 'No', 'ns_assistance': 'Yes' if record.ns_request_assistance else 'No', 'dtype_id': record.dtype_id, - 'visibility': record.visibility, + 'visibility': record.get_visibility_display(), 'other_fields': { - 'appeal': record.appeal, + 'appeal': record.get_appeal_display(), 'appeal_amount': record.appeal_amount, - 'bulletin': record.bulletin, + 'bulletin': record.get_bulletin_display(), 'countries': ', '.join(i.name for i in record.countries.all()), 'contacts': [{'ctype': c.ctype, 'name': c.name, @@ -492,36 +492,36 @@ def construct_template_record(self, rtype, record): 'phone': c.phone} for c in record.contacts.all()[::-1]], #not used this way, but shortened: 'description': record.description, 'districts': ', '.join(i.name for i in record.districts.all()), - 'dref': record.dref, + 'dref': record.get_dref_display(), 'dref_amount': record.dref_amount, 'epi_cases_since_last_fr': record.epi_cases_since_last_fr, 'epi_deaths_since_last_fr': record.epi_deaths_since_last_fr, 'epi_notes_since_last_fr': record.epi_notes_since_last_fr.split("\n") if record.epi_notes_since_last_fr else None, - 'eru_base_camp': record.eru_base_camp, + 'eru_base_camp': record.get_eru_base_camp_display(), 'eru_base_camp_units': record.eru_base_camp_units, - 'eru_basic_health_care': record.eru_basic_health_care, + 'eru_basic_health_care': record.get_eru_basic_health_care_display(), 'eru_basic_health_care_units': record.eru_basic_health_care_units, - 'eru_deployment_hospital': record.eru_deployment_hospital, + 'eru_deployment_hospital': record.get_eru_deployment_hospital_display(), 'eru_deployment_hospital_units': record.eru_deployment_hospital_units, - 'eru_it_telecom': record.eru_it_telecom, + 'eru_it_telecom': record.get_eru_it_telecom_display(), 'eru_it_telecom_units': record.eru_it_telecom_units, - 'eru_logistics': record.eru_logistics, + 'eru_logistics': record.get_eru_logistics_display(), 'eru_logistics_units': record.eru_logistics_units, - 'eru_referral_hospital': record.eru_referral_hospital, + 'eru_referral_hospital': record.get_eru_referral_hospital_display(), 'eru_referral_hospital_units': record.eru_referral_hospital_units, - 'eru_relief': record.eru_relief, + 'eru_relief': record.get_eru_relief_display(), 'eru_relief_units': record.eru_relief_units, - 'eru_water_sanitation_15': record.eru_water_sanitation_15, + 'eru_water_sanitation_15': record.get_eru_water_sanitation_15_display(), 'eru_water_sanitation_15_units': record.eru_water_sanitation_15_units, - 'eru_water_sanitation_20': record.eru_water_sanitation_20, + 'eru_water_sanitation_20': record.get_eru_water_sanitation_20_display(), 'eru_water_sanitation_20_units': record.eru_water_sanitation_20_units, - 'eru_water_sanitation_40': record.eru_water_sanitation_40, + 'eru_water_sanitation_40': record.get_eru_water_sanitation_40_display(), 'eru_water_sanitation_40_units': record.eru_water_sanitation_40_units, 'event': record.event, 'external_partners': ', '.join(i.name for i in record.external_partners.all()), - 'forecast_based_action': record.forecast_based_action, + 'forecast_based_action': record.get_forecast_based_action_display(), 'forecast_based_action_amount': record.forecast_based_action_amount, - 'imminent_dref': record.imminent_dref, + 'imminent_dref': record.get_imminent_dref_display(), 'imminent_dref_amount': record.imminent_dref_amount, 'notes_health': record.notes_health, 'notes_ns': record.notes_ns, @@ -529,14 +529,14 @@ def construct_template_record(self, rtype, record): 'other_sources': record.other_sources.split("\n") if record.other_sources else None, 'start_date': record.start_date, 'supported_activities': ', '.join(i.name for i in record.supported_activities.all()), - # not used: 'fact': record.fact, + # not used: 'fact': record.fact, !get_..._display! # not used: 'affected_pop_centres': record.affected_pop_centres, # not used: 'gov_affected_pop_centres': record.gov_affected_pop_centres, # not used: 'gov_num_highest_risk': record.gov_num_highest_risk, # not used: 'gov_num_potentially_affected': record.gov_num_potentially_affected, # not used: 'health_min_num_assisted': record.health_min_num_assisted, # not used: 'who_num_assisted': record.who_num_assisted, - # not used: 'ifrc_staff': record.ifrc_staff, + # not used: 'ifrc_staff': record.ifrc_staff, !get_..._display! # not used: 'is_covid_report': record.is_covid_report, # not used: 'num_expats_delegates': record.num_expats_delegates, # not used: 'num_fact': record.num_fact, @@ -544,7 +544,7 @@ def construct_template_record(self, rtype, record): # not used: 'num_ifrc_staff': record.num_ifrc_staff, # not used: 'num_localstaff': record.num_localstaff, # not used: 'num_potentially_affected': record.num_potentially_affected, - # not used: 'rdrt': record.rdrt, + # not used: 'rdrt': record.rdrt, !get_..._display! # not used: 'num_rdrt': record.num_rdrt, # not used: 'num_volunteers': record.num_volunteers, # not used: 'other_affected_pop_centres': record.other_affected_pop_centres, @@ -727,7 +727,7 @@ def notify(self, records, rtype, stype, uid=None): # Handle Visibility for Field Reports if rtype == RecordType.FIELD_REPORT: rtype_of_subscr, stype = self.fix_types_for_subs(rtype, stype) - non_ifrc_records = [rec for rec in record_entries if int(rec['visibility']) != 2] + non_ifrc_records = [rec for rec in record_entries if rec['visibility'] != 'IFRC Only'] if non_ifrc_records: non_ifrc_filters = (Q(subscription__rtype=rtype_of_subscr) & Q(subscription__stype=stype) & diff --git a/api/management/commands/ingest_appeals.py b/api/management/commands/ingest_appeals.py index 8315c0a56..b50688d8b 100644 --- a/api/management/commands/ingest_appeals.py +++ b/api/management/commands/ingest_appeals.py @@ -87,7 +87,7 @@ def get_new_or_modified_appeals(self): # get latest BILATERALS logger.info('Querying appeals API for new appeals data (bilateral)') url = 'http://go-api.ifrc.org/api/appealbilaterals' - auth = (os.getenv('APPEALS_USER'), os.getenv('APPEALS_PASS')) + auth = (settings.APPEALS_USER, settings.APPEALS_PASS) adapter = HTTPAdapter(max_retries=settings.RETRY_STRATEGY) sess = Session() diff --git a/api/management/commands/sync_molnix.py b/api/management/commands/sync_molnix.py index 825655573..2b6ac6dae 100644 --- a/api/management/commands/sync_molnix.py +++ b/api/management/commands/sync_molnix.py @@ -107,6 +107,7 @@ def get_datetime(datetime_string): return None return date_parser.parse(datetime_string) + def get_status_message(positions_messages, deployments_messages, positions_warnings, deployments_warnings): msg = '' msg += 'Summary of Open Positions Import:\n\n' @@ -125,26 +126,21 @@ def get_status_message(positions_messages, deployments_messages, positions_warni msg += '\n\n' return msg -def add_tags_to_obj(obj, tags): - # We clear all tags first, and then re-add them - tag_molnix_ids = [t['id'] for t in tags] - obj.molnix_tags.clear() - for molnix_id in tag_molnix_ids: - try: - t = MolnixTag.objects.get(molnix_id=molnix_id) - except: - logger.error('ERROR - tag not found: %d' % molnix_id) - continue - obj.molnix_tags.add(t) - obj.save() +def add_tags_to_obj(obj, tags): + _ids = [int(t['id']) for t in tags] + tags = list( # Fetch all at once + MolnixTag.objects.filter(molnix_id__in=_ids) + ) + if len(tags) != len(_ids): # Show warning if all tags are not available + missing_tag_ids = list(set(_ids) - set([tag.id for tag in tags])) + logger.warning(f'Missing _ids: {missing_tag_ids}') + # or ^^^^^^^ logger.error if we need to add molnix tags manually. + obj.molnix_tags.set(tags) # Add new ones, remove old ones def sync_deployments(molnix_deployments, molnix_api, countries): - #import json - #print(json.dumps(molnix_deployments, indent=2)) molnix_ids = [d['id'] for d in molnix_deployments] - warnings = [] messages = [] successful_creates = 0 diff --git a/api/migrations/0161_alter_event_options.py b/api/migrations/0161_alter_event_options.py new file mode 100644 index 000000000..3ff0e6ce9 --- /dev/null +++ b/api/migrations/0161_alter_event_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.16 on 2023-01-17 10:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0160_merge_0159_auto_20221022_1542_0159_auto_20221028_0940'), + ] + + operations = [ + migrations.AlterModelOptions( + name='event', + options={'ordering': ('-disaster_start_date', 'id'), 'verbose_name': 'emergency', 'verbose_name_plural': 'emergencies'}, + ), + ] diff --git a/api/models.py b/api/models.py index 66e862829..d3dc362f5 100644 --- a/api/models.py +++ b/api/models.py @@ -605,7 +605,7 @@ class Event(models.Model): ) class Meta: - ordering = ('-disaster_start_date',) + ordering = ('-disaster_start_date', 'id',) verbose_name = _('emergency') verbose_name_plural = _('emergencies') @@ -1925,6 +1925,7 @@ def __str__(self): return '%s | %s | %s' % (self.name, self.get_status_display(), str(self.created_at)[5:16]) # omit irrelevant 0 # Given a request containing new CronJob log row, validate and add the CronJob log row. + @staticmethod def sync_cron(body): new = [] errors = [] diff --git a/api/search_indexes.py b/api/search_indexes.py new file mode 100644 index 000000000..8d8c753e3 --- /dev/null +++ b/api/search_indexes.py @@ -0,0 +1,112 @@ +from haystack import indexes + +from api.models import ( + Country, + Event, + Appeal, + FieldReport, + Region, + District, +) + +class RegionIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.EdgeNgramField(document=True, use_template=True) + name = indexes.EdgeNgramField(model_attr='get_name_display') + + def get_model(self): + return Region + + def index_queryset(self, using=None): + return self.get_model().objects.all() + + +class CountryIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.EdgeNgramField(document=True, use_template=True) + name = indexes.EdgeNgramField(model_attr='name') + society_name = indexes.CharField(model_attr='society_name', null=True) + independent = indexes.BooleanField(model_attr='independent', null=True) + is_deprecated = indexes.BooleanField(model_attr='is_deprecated', null=True) + + def get_model(self): + return Country + + def index_queryset(self, using=None): + return self.get_model().objects.all() + + +class DistrictIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.EdgeNgramField(document=True, use_template=True) + name = indexes.EdgeNgramField(model_attr='name') + country_name = indexes.CharField(model_attr='country__name', null=True) + country_id = indexes.CharField(model_attr='country__id', null=True) + + def get_model(self): + return District + + def index_queryset(self, using=None): + return self.get_model().objects.all() + + +class AppealIndex(indexes.Indexable, indexes.SearchIndex): + text = indexes.EdgeNgramField(document=True, use_template=True) + name = indexes.EdgeNgramField(model_attr='name') + visibility = indexes.CharField(model_attr='event__visibility', null=True) + appeal_type = indexes.CharField(model_attr='get_atype_display') + code = indexes.CharField(model_attr='code') + event_name = indexes.CharField(model_attr='event__name', null=True) + country_name = indexes.CharField(model_attr='country__name') + start_date = indexes.DateTimeField(model_attr='start_date', null=True) + country_id = indexes.IntegerField(model_attr='country__id') + event_id = indexes.IntegerField(model_attr='event__id', null=True) + + def get_model(self): + return Appeal + + def index_queryset(self, using=None): + return self.get_model().objects.all() + +class EmergenciesIndex(indexes.Indexable, indexes.SearchIndex): + text = indexes.EdgeNgramField(document=True, use_template=True) + name = indexes.EdgeNgramField(model_attr='name') + visibility = indexes.CharField(model_attr='visibility', null=True) + countries = indexes.MultiValueField(null=True,) + disaster_start_date = indexes.DateTimeField(model_attr='disaster_start_date', null=True) + amount_requested = indexes.CharField(model_attr='appeals__amount_requested', null=True) + amount_funded = indexes.CharField(model_attr='appeals__amount_funded', null=True) + disaster_type = indexes.CharField(model_attr='dtype__name', null=True) + countries_id = indexes.MultiValueField(null=True,) + + def get_model(self): + return Event + + def prepare_countries(self, obj): + return [country.name for country in obj.countries.all()] + + def prepare_countries_id(self, obj): + return [country.id for country in obj.countries.all()] + + def index_queryset(self, using=None): + return self.get_model().objects.all() + + +class FieldReportIndex(indexes.Indexable, indexes.SearchIndex): + text = indexes.EdgeNgramField(document=True, use_template=True) + name = indexes.EdgeNgramField(model_attr='summary') + visibility = indexes.CharField(model_attr='visibility', null=True) + countries = indexes.MultiValueField(null=True) + event_name = indexes.CharField(model_attr='event__name', null=True) + created_at = indexes.DateTimeField(model_attr='created_at') + event_id = indexes.IntegerField(model_attr='event__id', null=True) + countries_id = indexes.MultiValueField(null=True,) + + def get_model(self): + return FieldReport + + def index_queryset(self, using=None): + return self.get_model().objects.all() + + def prepare_countries(self, obj): + return [country.name for country in obj.countries.all()] + + def prepare_countries_id(self, obj): + return [country.id for country in obj.countries.all()] diff --git a/api/serializers.py b/api/serializers.py index c2c9ebf6f..529760534 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -52,7 +52,7 @@ CountryOfFieldReportToReview, ) from notifications.models import Subscription -from deployments.models import EmergencyProject +from deployments.models import EmergencyProject, Personnel class GeoSerializerMixin: @@ -357,6 +357,7 @@ class RegionRelationSerializer(ModelSerializer): preparedness_snippets = RegionPreparednessSnippetSerializer(many=True, read_only=True) national_society_count = serializers.SerializerMethodField() country_cluster_count = serializers.SerializerMethodField() + country_plan_count = serializers.IntegerField(read_only=True) @staticmethod def get_national_society_count(obj): @@ -370,13 +371,14 @@ class Meta: model = Region fields = ('links', 'contacts', 'snippets', 'emergency_snippets', 'profile_snippets', 'preparedness_snippets', 'name', - 'region_name', 'id', 'additional_tab_name', + 'region_name', 'id', 'additional_tab_name', 'country_plan_count', 'national_society_count', 'country_cluster_count',) class CountryRelationSerializer(ModelSerializer): links = CountryLinkSerializer(many=True, read_only=True) contacts = CountryContactSerializer(many=True, read_only=True) + has_country_plan = serializers.BooleanField(read_only=True) class Meta: model = Country @@ -387,7 +389,7 @@ class Meta: 'nsi_trained_in_first_aid', 'nsi_gov_financial_support', 'nsi_domestically_generated_income', 'nsi_annual_fdrs_reporting', 'nsi_policy_implementation', 'nsi_risk_management_framework', 'nsi_cmc_dashboard_compliance', 'wash_kit2', 'wash_kit5', 'wash_kit10', 'wash_staff_at_hq', - 'wash_staff_at_branch', 'wash_ndrt_trained', 'wash_rdrt_trained', + 'wash_staff_at_branch', 'wash_ndrt_trained', 'wash_rdrt_trained', 'has_country_plan', ) @@ -397,7 +399,8 @@ class RelatedAppealSerializer(ModelSerializer): class Meta: model = Appeal fields = ( - 'aid', 'num_beneficiaries', 'amount_requested', 'amount_funded', 'status', 'status_display', 'start_date', 'id', + 'aid', 'num_beneficiaries', 'amount_requested', 'code', + 'amount_funded', 'status', 'status_display', 'start_date', 'id', ) @@ -498,6 +501,7 @@ class ListEventSerializer(ModelSerializer): field_reports = MiniFieldReportSerializer(many=True, read_only=True) dtype = DisasterTypeSerializer() ifrc_severity_level_display = serializers.CharField(source='get_ifrc_severity_level_display', read_only=True) + active_deployments = serializers.SerializerMethodField() class Meta: model = Event @@ -505,9 +509,19 @@ class Meta: 'name', 'dtype', 'countries', 'summary', 'num_affected', 'ifrc_severity_level', 'ifrc_severity_level_display', 'glide', 'disaster_start_date', 'created_at', 'auto_generated', 'appeals', 'is_featured', 'is_featured_region', 'field_reports', 'updated_at', 'id', 'slug', 'parent_event', 'tab_one_title', 'tab_two_title', 'tab_three_title', - 'emergency_response_contact_email', + 'emergency_response_contact_email', 'active_deployments', ) + def get_active_deployments(self, event): + now = timezone.now() + return Personnel.objects.filter( + type=Personnel.TypeChoices.RR, + start_date__lt=now, + end_date__gt=now, + deployment__event_deployed_to=event, + is_active=True + ).count() + class SurgeEventSerializer(ModelSerializer): appeals = RelatedAppealSerializer(many=True, read_only=True) @@ -649,7 +663,7 @@ def get_organizations_from(obj): deployments = [d for d in obj.personneldeployment_set.all()] personnels = [] for d in deployments: - for p in d.personnel_set.filter(end_date__gte=timezone.now(), is_active=True): + for p in d.personnel_set.filter(end_date__gte=timezone.now(), start_date__lte=timezone.now(), is_active=True): personnels.append(p) return list(set([p.country_from.society_name for p in personnels if p.country_from and p.country_from.society_name != ''])) @@ -672,6 +686,7 @@ class DetailEventSerializer(ModelSerializer): links = EventLinkSerializer(many=True, read_only=True) countries_for_preview = MiniCountrySerializer(many=True) response_activity_count = serializers.SerializerMethodField() + active_deployments = serializers.SerializerMethodField() class Meta: model = Event @@ -681,13 +696,22 @@ class Meta: 'is_featured_region', 'field_reports', 'hide_attached_field_reports', 'hide_field_report_map', 'updated_at', 'id', 'slug', 'tab_one_title', 'ifrc_severity_level', 'ifrc_severity_level_display', 'parent_event', 'glide', 'featured_documents', 'links', 'emergency_response_contact_email', 'countries_for_preview', - 'response_activity_count', 'visibility' + 'response_activity_count', 'visibility', 'active_deployments' ) lookup_field = 'slug' def get_response_activity_count(self, event): return EmergencyProject.objects.filter(event=event).count() + def get_active_deployments(self, event): + now = timezone.now() + return Personnel.objects.filter( + type=Personnel.TypeChoices.RR, + start_date__lt=now, + end_date__gt=now, + deployment__event_deployed_to=event, + is_active=True + ).count() class SituationReportTypeSerializer(ModelSerializer): class Meta: diff --git a/api/snapshots/snap_test_views.py b/api/snapshots/snap_test_views.py index 7090f2d1b..7da2ac45b 100644 --- a/api/snapshots/snap_test_views.py +++ b/api/snapshots/snap_test_views.py @@ -18,6 +18,7 @@ "disaster_start_date": "2015-04-21T17:45:23.476445Z", "created_at": "2008-01-01T00:00:00.123456Z", "auto_generated": True, + 'active_deployments': 0, "appeals": [], "contacts": [], "key_figures": [], @@ -90,6 +91,7 @@ "disaster_start_date": "2015-04-21T17:45:23.476445Z", "created_at": "2008-01-01T00:00:00.123456Z", "auto_generated": True, + 'active_deployments': 0, "appeals": [], "contacts": [], "key_figures": [], diff --git a/api/templates/admin/app_index.html b/api/templates/admin/app_index.html new file mode 100644 index 000000000..3d90b4b3e --- /dev/null +++ b/api/templates/admin/app_index.html @@ -0,0 +1,23 @@ +{% extends "admin/index.html" %} +{% load i18n %} + +{% block bodyclass %}{{ block.super }} app-{{ app_label }}{% endblock %} + +{% if not is_popup %} +{% block breadcrumbs %} + +{% if app_list.0.app_label == 'country_plan' %} +
+ +
+{% endif %} +{% endblock %} +{% endif %} +{% block sidebar %}{% endblock %} diff --git a/api/templates/admin/countryplan_change_form.html b/api/templates/admin/countryplan_change_form.html new file mode 100644 index 000000000..b52f8116c --- /dev/null +++ b/api/templates/admin/countryplan_change_form.html @@ -0,0 +1,7 @@ +{% extends "admin/change_form.html" %} +{% block object-tools-items %} +
  • + ? +
  • +{{ block.super }} +{% endblock %} diff --git a/api/templates/admin/countryplan_change_list.html b/api/templates/admin/countryplan_change_list.html new file mode 100644 index 000000000..b4bbd83e9 --- /dev/null +++ b/api/templates/admin/countryplan_change_list.html @@ -0,0 +1,7 @@ +{% extends "reversion/change_list.html" %} +{% block object-tools-items %} +
  • + ? +
  • +{{ block.super }} +{% endblock %} diff --git a/api/templates/search/indexes/api/appeal_text.txt b/api/templates/search/indexes/api/appeal_text.txt new file mode 100644 index 000000000..387ed049c --- /dev/null +++ b/api/templates/search/indexes/api/appeal_text.txt @@ -0,0 +1,8 @@ +{{object.id}} +{{object.name}} +{{object.visibility}} +{{object.appeal_type}} +{{object.code}} +{{object.event_id}} +{{object.country_id}} +{{object.start_date}} diff --git a/api/templates/search/indexes/api/country_text.txt b/api/templates/search/indexes/api/country_text.txt new file mode 100644 index 000000000..2cf760691 --- /dev/null +++ b/api/templates/search/indexes/api/country_text.txt @@ -0,0 +1,4 @@ +{{object.id}} +{{object.name}} +{{object.society_name}} +{{object.society_url}} diff --git a/api/templates/search/indexes/api/district_text.txt b/api/templates/search/indexes/api/district_text.txt new file mode 100644 index 000000000..6f6a2b046 --- /dev/null +++ b/api/templates/search/indexes/api/district_text.txt @@ -0,0 +1,2 @@ +{{object.id}} +{{object.name}} diff --git a/api/templates/search/indexes/api/event_text.txt b/api/templates/search/indexes/api/event_text.txt new file mode 100644 index 000000000..910a6d937 --- /dev/null +++ b/api/templates/search/indexes/api/event_text.txt @@ -0,0 +1,5 @@ +{{object.name}} +{{object.disaster_type}} +{{object.amount_requested}} +{{object.amount_funded}} +{{object.disaster_start_date}} \ No newline at end of file diff --git a/api/templates/search/indexes/api/fieldreport_text.txt b/api/templates/search/indexes/api/fieldreport_text.txt new file mode 100644 index 000000000..ef664e650 --- /dev/null +++ b/api/templates/search/indexes/api/fieldreport_text.txt @@ -0,0 +1 @@ +{{object.name}} \ No newline at end of file diff --git a/api/templates/search/indexes/api/region_text.txt b/api/templates/search/indexes/api/region_text.txt new file mode 100644 index 000000000..feab1d0bf --- /dev/null +++ b/api/templates/search/indexes/api/region_text.txt @@ -0,0 +1 @@ +{{object.name.get_name_display}} diff --git a/api/views.py b/api/views.py index a7a9bb58f..5cdeb4cc9 100644 --- a/api/views.py +++ b/api/views.py @@ -17,16 +17,31 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import authentication, permissions - -from deployments.models import Heop, ERUType, ProgrammeTypes, Sectors, OperationTypes, Statuses -from notifications.models import Subscription +from rest_framework.pagination import LimitOffsetPagination + +from deployments.models import ( + Heop, + ERUType, + ProgrammeTypes, + Sectors, + OperationTypes, + Statuses +) +from notifications.models import Subscription, SurgeAlert from notifications.notification import send_notification from registrations.models import Recovery, Pending +from deployments.models import Project, ERU +from flash_update.models import FlashUpdate +from dref.models import Dref, DrefOperationalUpdate from .esconnection import ES_CLIENT from .models import Appeal, AppealType, Event, FieldReport, CronJob, AppealHistory from .indexes import ES_PAGE_NAME from .logger import logger +from haystack.query import SearchQuerySet +from api.models import Country, Region, District +from haystack.inputs import AutoQuery, Raw +from haystack.query import SQ def bad_request(message): @@ -140,6 +155,219 @@ def get(self, request): ) return JsonResponse(results['hits']) +class HayStackSearch(APIView): + + def get(self, request): + phrase = request.GET.get('keyword', None) + phrase = phrase.lower() + if phrase is None: + return bad_request('Must include a `keyword`') + + if phrase: + region_response = SearchQuerySet().models(Region).filter( + SQ(name__startswith=phrase) + ) + country_response = SearchQuerySet().models(Country).filter( + SQ(name__contains=phrase), + SQ(independent='true'), + SQ(is_depercent='false') + ).order_by('-_score') + emergency_response = SearchQuerySet().models(Event).filter( + SQ(name__content=phrase)).order_by('-_score') + appeal_response = SearchQuerySet().models(Appeal).filter( + SQ(name__content=phrase)).order_by('-_score') + fieldreport_response = SearchQuerySet().models(FieldReport).filter( + SQ(name__content=phrase)).order_by('-_score') + surge_alert_response = SearchQuerySet().models(SurgeAlert).filter( + SQ(event_name__content=phrase) | SQ(country_name__content=phrase) + ).order_by('-_score') + project_response = SearchQuerySet().models(Project).filter( + SQ(event_name__content=phrase) | SQ(name__content=phrase) + ).order_by('-_score') + surge_deployments = SearchQuerySet().models(ERU).filter( + SQ(event_name__content=phrase) | SQ(country__content=phrase) + ).order_by('-_score') + district_province_response = SearchQuerySet().models(District).filter( + SQ(name__contains=phrase) + ).order_by('-_score') + flash_update_response = SearchQuerySet().models(FlashUpdate).filter( + SQ(name__contains=phrase) + ).order_by('-_score') + dref_response = SearchQuerySet().models(Dref).filter( + SQ(name__contains=phrase) + ).order_by('-_score') + dref_operational_update_response = SearchQuerySet().models(DrefOperationalUpdate).filter( + SQ(name__contains=phrase) + ).order_by('-_score') + + appeals_list = [] + dref = [ + { + "id": int(data.id.split(".")[-1]), + "name": data.name, + "created_at": data.created_at, + "type": "Dref", + "score": data.score, + "country_name": data.country_name, + "country_id": data.country_id, + "code": data.code + } for data in dref_response + ] + appeals_list.extend(dref) + dref_op = [ + { + "id": int(data.id.split(".")[-1]), + "name": data.name, + "created_at": data.created_at, + "type": "Operational Update", + "score": data.score, + "country_name": data.country_name, + "country_id": data.country_id, + "code": data.code + } for data in dref_operational_update_response + ] + appeals_list.extend(dref_op) + appeal_data = [ + { + "id": int(data.id.split(".")[-1]), + "name": data.name, + "code": data.code, + "country": data.country_name, + "country_id": data.country_id, + "start_date": data.start_date, + "score": data.score, + "type": "Appeal", + } for data in appeal_response + ] + appeals_list.extend(appeal_data) + field_report = [] + flash_update = [ + { + "id": int(data.id.split(".")[-1]), + "name": data.name, + "created_at": data.created_at, + "type": "Flash Update", + "score": data.score, + } for data in flash_update_response + ] + field_report.extend(flash_update) + field_reports_data = [ + { + "id": int(data.id.split(".")[-1]), + "name": data.name, + "created_at": data.created_at, + "type": "Field Report", + "score": data.score, + } for data in fieldreport_response.order_by('-created_at') + ] + field_report.extend(field_reports_data) + result = { + "regions": [ + { + "id": int(data.id.split(".")[-1]), + "name": data.name, + "score": data.score + } for data in region_response[:50] + ], + "district_province_response": [ + { + "id": int(data.id.split(".")[-1]), + "name": data.name, + "score": data.score, + "country": data.country_name, + "country_id": data.country_id, + } for data in district_province_response[:50] + ], + "countries": [ + { + "id": int(data.id.split(".")[-1]), + "name": data.name, + "society_name": data.society_name, + "score": data.score, + } for data in country_response[:50] + ], + "emergencies": [ + { + "id": int(data.id.split(".")[-1]), + "name": data.name, + "disaster_type": data.disaster_type, + "funding_requirements": data.amount_requested, + "funding_coverage": data.amount_funded, + "event_date": data.disaster_start_date, + "score": data.score, + "countries": data.countries, + "countries_id": data.countries_id + } for data in emergency_response[:50] + ], + "surge_alerts": [ + { + "id": int(data.id.split(".")[-1]), + "name": data.name, + "keywords": data.molnix_tag, + "event_name": data.event_name, + "country": data.country_name, + "start_date": data.start_date, + "alert_date": data.alert_date, + "score": data.score, + "event_id": data.event_id, + "status": data.status, + "deadline": data.deadline, + "surge_type": data.surge_type, + "country_id": data.country_id, + } for data in surge_alert_response.order_by('-start_date')[:50] + ], + "projects": [ + { + "id": int(data.id.split(".")[-1]), + "name": data.name, + "event_name": data.event_name, + "national_society": data.reporting_ns, + "tags": data.tags, + "sector": data.sector, + "start_date": data.start_date, + "end_date": data.end_date, + "regions": data.project_districts, + "people_targeted": data.target_total, + "score": data.score, + "event_id": data.event_id, + "national_society_id": data.reporting_ns_id + } for data in project_response.order_by('-start_date')[:50] + ], + "surge_deployments": [ + { + "id": int(data.id.split(".")[-1]), + "event_name": data.event_name, + "deployed_country": data.country, + "type": data.eru_type, + "owner": data.eru_owner, + "personnel_units": data.personnel_units, + "equipment_units": data.equipment_units, + "event_id": data.event_id, + "score": data.score, + "deployed_country_id": data.country_id, + } for data in surge_deployments[:50] + ], + "reports": sorted(field_report, key=lambda d: d["score"], reverse=True)[:50], + "emergency_planning": sorted(appeals_list, key=lambda d: d["score"], reverse=True)[:50] + } + return Response(result) + + +class Brief(APIView): + @classmethod + def get(cls, request): + e = Event.objects.filter(summary__contains='base64').count() + f = FieldReport.objects.filter(description__contains='base64').count() + u = FlashUpdate.objects.filter(situational_overview__contains='base64').count() + c = CronJob.objects.filter(status=2).count() + res = ES_CLIENT.cluster.health() + res['--------------------------------'] = '----------' + res['base64_img'] = e + f + u + res['cronjob_err'] = c + res['git_last_tag'] = settings.LAST_GIT_TAG + res['git_last_commit'] = settings.SENTRY_CONFIG['release'][0:8] + return JsonResponse(res, safe=False) + class ERUTypes(APIView): @classmethod diff --git a/country_plan/__init__.py b/country_plan/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/country_plan/admin.py b/country_plan/admin.py new file mode 100644 index 000000000..211bc8537 --- /dev/null +++ b/country_plan/admin.py @@ -0,0 +1,39 @@ +from django.contrib import admin + +from country_plan.models import ( + CountryPlan, + DataImport, + StrategicPriority, + MembershipCoordination +) + + +@admin.register(DataImport) +class DataImportAdmin(admin.ModelAdmin): + readonly_fields = ('errors',) + + change_form_template = "admin/countryplan_change_form.html" + change_list_template = "admin/countryplan_change_list.html" + + +@admin.register(CountryPlan) +class CountryPlanAdmin(admin.ModelAdmin): + autocomplete_fields = ('country',) + search_fields = ('country__name',) + list_filter = ('is_publish',) + + change_form_template = "admin/countryplan_change_form.html" + change_list_template = "admin/countryplan_change_list.html" + + +@admin.register(StrategicPriority) +class StrategicPriorityAdmin(admin.ModelAdmin): + list_display = ('country_plan', 'type', 'people_targeted') + autocomplete_fields = ('country_plan',) + + +@admin.register(MembershipCoordination) +class MembershipCoordinationAdmin(admin.ModelAdmin): + list_display = ('country_plan', 'national_society', 'sector') + search_fields = ('sector',) + autocomplete_fields = ('national_society', 'country_plan') diff --git a/country_plan/apps.py b/country_plan/apps.py new file mode 100644 index 000000000..fc07cb07d --- /dev/null +++ b/country_plan/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CountryPlanConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'country_plan' diff --git a/country_plan/drf_views.py b/country_plan/drf_views.py new file mode 100644 index 000000000..ae6b73bc2 --- /dev/null +++ b/country_plan/drf_views.py @@ -0,0 +1,18 @@ +from rest_framework import mixins, viewsets +from django.db import models + +from .models import CountryPlan, MembershipCoordination +from .serializers import CountryPlanSerializer + + +class CountryPlanViewset(mixins.RetrieveModelMixin, viewsets.GenericViewSet): + serializer_class = CountryPlanSerializer + queryset = CountryPlan.objects.prefetch_related( + 'country_plan_sp', + models.Prefetch( + 'country_plan_mc', + queryset=MembershipCoordination.objects.annotate( + national_society_name=models.F('national_society__society_name'), + ), + ), + ).filter(is_publish=True) diff --git a/country_plan/factories.py b/country_plan/factories.py new file mode 100644 index 000000000..86191c658 --- /dev/null +++ b/country_plan/factories.py @@ -0,0 +1,8 @@ +import factory + +from .models import CountryPlan + + +class CountryPlanFactory(factory.django.DjangoModelFactory): + class Meta: + model = CountryPlan diff --git a/country_plan/management/__init__.py b/country_plan/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/country_plan/management/commands/__init__.py b/country_plan/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/country_plan/management/commands/ingest_country_plan_file.py b/country_plan/management/commands/ingest_country_plan_file.py new file mode 100644 index 000000000..0e1cb7995 --- /dev/null +++ b/country_plan/management/commands/ingest_country_plan_file.py @@ -0,0 +1,132 @@ +import logging +import requests +from typing import Union, Optional, Tuple +from datetime import datetime +from django.utils.timezone import make_aware +from django.core.management.base import BaseCommand +from django.core.files.base import File +from django.conf import settings +from django.db import models + +from main.utils import DownloadFileManager +from api.models import Country +from country_plan.models import CountryPlan + +logger = logging.getLogger(__name__) + +# Ref: https://github.com/IFRCGo/go-api/issues/1614 +PUBLIC_SOURCE = 'https://go-api.ifrc.org/api/publicsiteappeals?AppealsTypeID=1851&Hidden=false' +# Ref: https://github.com/IFRCGo/go-api/issues/1648 +INTERNAL_SOURCE = 'https://go-api.ifrc.org/Api/FedNetAppeals?AppealsTypeId=1844&Hidden=false' + + +def parse_date(text: Optional[str]) -> Optional[datetime]: + """ + Convert Appeal API datetime into django datetime + Parameters + ---------- + text : str + Datetime eg: 2022-11-29T11:24:00 + """ + if text: + return make_aware( + # NOTE: Format is assumed by looking at the data from Appeal API + datetime.strptime(text, '%Y-%m-%dT%H:%M:%S') + ) + + +def get_meta_from_url(url) -> Tuple[Optional[str], str]: + """ + Fetch url headers and return content-type and filename + """ + def _get_filename_from_headers(resp): + try: + # Eg: Content-Disposition: 'attachment;filename=UP_Botswana_2023.pdf' + return resp.headers.get('Content-Disposition').split(';')[1].split('=')[1] + except Exception: + return 'document.pdf' + + # Check if it is a pdf file + resp = requests.head(url) + return resp.headers.get('Content-Type'), _get_filename_from_headers(resp) + + +class Command(BaseCommand): + @staticmethod + def load_file_to_country_plan(country_plan: CountryPlan, url: str, filename: str, field_name: str): + """ + Fetch file using url and save to country_plan + """ + with DownloadFileManager(url, suffix='.pdf') as f: + getattr(country_plan, field_name).save( + filename, + File(f), + ) + + def load_for_country(self, country_data: dict, file_field: str, field_inserted_date_field: str): + country_iso2 = country_data.get('LocationCountryCode') + country_name = country_data.get('LocationCountryName') + plan_url = country_data['BaseDirectory'] + country_data['BaseFileName'] + inserted_date = parse_date(country_data.get('Inserted')) + if ( + (country_iso2 is None and country_name is None) or + plan_url is None or + inserted_date is None + ): + return + country_qs = Country.objects.filter( + models.Q(iso__iexact=country_iso2) | + models.Q(name__iexact=country_name) + ) + country_plan = CountryPlan.objects.filter(country__in=country_qs).first() + if country_plan is None: + country = country_qs.first() + # If there is no country as well, show warning and return + if not country: + logger.warning(f'{file_field} No country found for: {(country_iso2, country_name)}') + return + # Else create one and continue + country_plan = CountryPlan(country=country) + existing_inserted_date = getattr(country_plan, field_inserted_date_field, None) + if existing_inserted_date and existing_inserted_date >= inserted_date: + # No need to do anything here + return + content_type, content_name = get_meta_from_url(plan_url) + self.stdout.write(f'- Checking plan file for country:: {country_plan.country.name}') + if content_type != 'application/pdf': + # Only looking for PDFs + return + self.stdout.write(' - Saving data') + setattr(country_plan, field_inserted_date_field, inserted_date) + self.load_file_to_country_plan( + country_plan, + plan_url, + # NOTE: File provided are PDF, + f"{file_field.replace('_', '-').replace('-file', '')}-{content_name}", + file_field, + ) + country_plan.is_publish = True + country_plan.save( + update_fields=( + field_inserted_date_field, + file_field, # By load_file_to_country_plan + 'is_publish', + ) + ) + + def load(self, url: str, file_field: str, field_inserted_date_field: str): + auth = (settings.APPEALS_USER, settings.APPEALS_PASS) + results = requests.get(url, auth=auth, headers={'Accept': 'application/json'}).json() + for result in results: + try: + self.load_for_country(result, file_field, field_inserted_date_field) + except Exception: + logger.error('Could not Updated countries plan', exc_info=True) + + def handle(self, **_): + # Public + self.stdout.write('Fetching data for country plans:: PUBLIC') + self.load(PUBLIC_SOURCE, 'public_plan_file', 'public_plan_inserted_date') + # Private + self.stdout.write('\nFetching data for country plans:: PRIVATE') + self.load(INTERNAL_SOURCE, 'internal_plan_file', 'internal_plan_inserted_date') diff --git a/country_plan/migrations/0001_initial.py b/country_plan/migrations/0001_initial.py new file mode 100644 index 000000000..cdd022bc4 --- /dev/null +++ b/country_plan/migrations/0001_initial.py @@ -0,0 +1,78 @@ +# Generated by Django 3.2.16 on 2022-11-24 05:11 + +import country_plan.models +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('api', '0160_merge_0159_auto_20221022_1542_0159_auto_20221028_0940'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='CountryPlan', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('country', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='country_plan', serialize=False, to='api.country')), + ('internal_plan_file', models.FileField(blank=True, null=True, upload_to=country_plan.models.pdf_upload_to, verbose_name='Internal Plan')), + ('public_plan_file', models.FileField(blank=True, null=True, upload_to=country_plan.models.pdf_upload_to, verbose_name='Country Plan')), + ('requested_amount', models.FloatField(blank=True, null=True, verbose_name='Requested Amount')), + ('people_targeted', models.IntegerField(blank=True, null=True, verbose_name='People Targeted')), + ('is_publish', models.BooleanField(default=False)), + ('created_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='countryplan_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('updated_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='countryplan_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Updated by')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='DataImport', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('file', models.FileField(upload_to=country_plan.models.file_upload_to, verbose_name='EXCEL file')), + ('errors', models.JSONField(blank=True, null=True)), + ('created_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dataimport_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('updated_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dataimport_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Updated by')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='StrategicPriority', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.CharField(choices=[('climate_and_environmental_crisis', 'Climate and environmental crisis'), ('evolving_crisis_and_disasters', 'Evolving crisis and disasters'), ('growing_gaps_in_health_and_wellbeing', 'Growing gaps in health and wellbeing'), ('migration_and_identity', 'Migration and Identity'), ('value_power_and_inclusion', 'Value power and inclusion')], max_length=100, verbose_name='Type')), + ('funding_requirement', models.FloatField(blank=True, null=True, verbose_name='Funding Requirement')), + ('people_targeted', models.IntegerField(blank=True, null=True, verbose_name='People Targeted')), + ('country_plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='country_plan_sp', to='country_plan.countryplan', verbose_name='Country Plan')), + ], + options={ + 'unique_together': {('country_plan', 'type')}, + }, + ), + migrations.CreateModel( + name='MembershipCoordination', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('sector', models.CharField(choices=[('climate', 'Climate'), ('crisis', 'Crisis'), ('health', 'Health'), ('migration', 'Migration'), ('inclusion', 'Inclusion'), ('engaged', 'Engaged'), ('accountable', 'Accountable'), ('trusted', 'Trusted')], max_length=100, verbose_name='Sector')), + ('has_coordination', models.BooleanField(default=False)), + ('country_plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='country_plan_mc', to='country_plan.countryplan', verbose_name='Country Plan')), + ('national_society', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='national_society_mc', to='api.country')), + ], + options={ + 'unique_together': {('country_plan', 'national_society', 'sector')}, + }, + ), + ] diff --git a/country_plan/migrations/0002_alter_countryplan_is_publish.py b/country_plan/migrations/0002_alter_countryplan_is_publish.py new file mode 100644 index 000000000..eddda2a33 --- /dev/null +++ b/country_plan/migrations/0002_alter_countryplan_is_publish.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2022-11-25 13:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('country_plan', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='countryplan', + name='is_publish', + field=models.BooleanField(default=False, verbose_name='Published'), + ), + ] diff --git a/country_plan/migrations/0003_auto_20221128_0831.py b/country_plan/migrations/0003_auto_20221128_0831.py new file mode 100644 index 000000000..9478b4b45 --- /dev/null +++ b/country_plan/migrations/0003_auto_20221128_0831.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.16 on 2022-11-28 08:31 + +import country_plan.models +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('country_plan', '0002_alter_countryplan_is_publish'), + ] + + operations = [ + migrations.AlterField( + model_name='countryplan', + name='internal_plan_file', + field=models.FileField(blank=True, null=True, upload_to=country_plan.models.pdf_upload_to, validators=[django.core.validators.FileExtensionValidator(['pdf'])], verbose_name='Internal Plan'), + ), + migrations.AlterField( + model_name='countryplan', + name='public_plan_file', + field=models.FileField(blank=True, null=True, upload_to=country_plan.models.pdf_upload_to, validators=[django.core.validators.FileExtensionValidator(['pdf'])], verbose_name='Country Plan'), + ), + migrations.AlterField( + model_name='strategicpriority', + name='type', + field=models.CharField(choices=[('ongoing_emergency_operations', 'Ongoing emergency operations'), ('climate_and_environmental_crisis', 'Climate and environmental crisis'), ('evolving_crisis_and_disasters', 'Evolving crisis and disasters'), ('growing_gaps_in_health_and_wellbeing', 'Growing gaps in health and wellbeing'), ('migration_and_identity', 'Migration and Identity'), ('value_power_and_inclusion', 'Value power and inclusion')], max_length=100, verbose_name='Type'), + ), + ] diff --git a/country_plan/migrations/0004_countryplan_appeal_api_inserted_date.py b/country_plan/migrations/0004_countryplan_appeal_api_inserted_date.py new file mode 100644 index 000000000..d6e5b87c3 --- /dev/null +++ b/country_plan/migrations/0004_countryplan_appeal_api_inserted_date.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2023-01-02 07:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('country_plan', '0003_auto_20221128_0831'), + ] + + operations = [ + migrations.AddField( + model_name='countryplan', + name='appeal_api_inserted_date', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/country_plan/migrations/0005_auto_20230207_0840.py b/country_plan/migrations/0005_auto_20230207_0840.py new file mode 100644 index 000000000..c530f40c8 --- /dev/null +++ b/country_plan/migrations/0005_auto_20230207_0840.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.16 on 2023-02-07 08:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('country_plan', '0004_countryplan_appeal_api_inserted_date'), + ] + + operations = [ + migrations.RenameField( + model_name='countryplan', + old_name='appeal_api_inserted_date', + new_name='public_plan_inserted_date', + ), + migrations.AddField( + model_name='countryplan', + name='internal_plan_inserted_date', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/country_plan/migrations/__init__.py b/country_plan/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/country_plan/models.py b/country_plan/models.py new file mode 100644 index 000000000..816b3af5f --- /dev/null +++ b/country_plan/models.py @@ -0,0 +1,163 @@ +from django.db import models, transaction +from django.contrib.auth.models import User +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ +from django.core.validators import FileExtensionValidator + +from api.models import Country + + +def file_upload_to(instance, filename): + date_str = timezone.now().strftime('%Y-%m-%d-%H-%M-%S') + return f'country-plan-excel-files/{date_str}/{filename}' + + +def pdf_upload_to(instance, filename): + date_str = timezone.now().strftime('%Y-%m-%d-%H-%M-%S') + return f'country-plan-pdf/{date_str}/{filename}' + + +class CountryPlanAbstract(models.Model): + created_by = models.ForeignKey( + User, + verbose_name=_('Created By'), + blank=True, + null=True, + related_name='%(class)s_created_by', + on_delete=models.SET_NULL, + editable=False, + ) + created_at = models.DateTimeField(verbose_name=_('Created at'), auto_now_add=True) + updated_by = models.ForeignKey( + User, + verbose_name=_('Updated by'), + blank=True, + null=True, + related_name='%(class)s_updated_by', + on_delete=models.SET_NULL, + editable=False, + ) + updated_at = models.DateTimeField(verbose_name=_('Updated at'), auto_now=True) + + class Meta: + abstract = True + + +class DataImport(CountryPlanAbstract): + file = models.FileField(verbose_name=_('EXCEL file'), upload_to=file_upload_to) + errors = models.JSONField(null=True, blank=True) + + def __str__(self): + return self.file.name + + def save(self, *args, **kwargs): + from .tasks import process_data_import + new = False + if self.pk is None: + new = True + super().save(*args, **kwargs) + if new: + transaction.on_commit( + lambda: process_data_import.delay(self.pk) + ) + + +class CountryPlan(CountryPlanAbstract): + country = models.OneToOneField(Country, on_delete=models.CASCADE, related_name='country_plan', primary_key=True) + internal_plan_file = models.FileField( + verbose_name=_('Internal Plan'), + upload_to=pdf_upload_to, + validators=[FileExtensionValidator(['pdf'])], + blank=True, null=True + ) + public_plan_file = models.FileField( + verbose_name=_('Country Plan'), + validators=[FileExtensionValidator(['pdf'])], + upload_to=pdf_upload_to, + blank=True, null=True + ) + requested_amount = models.FloatField(verbose_name=_('Requested Amount'), blank=True, null=True) + people_targeted = models.IntegerField(verbose_name=_('People Targeted'), blank=True, null=True) + is_publish = models.BooleanField(default=False, verbose_name=_('Published')) + + # NOTE: Used to sync with Appeal API (ingest_country_plan_file.py for more info) + public_plan_inserted_date = models.DateTimeField(blank=True, null=True) + internal_plan_inserted_date = models.DateTimeField(blank=True, null=True) + + def __str__(self): + return f'{self.country}' + + def full_country_plan_mc(self): + all_mc = list(self.country_plan_mc.all()) + mc_by_ns_sector = { + (mc.national_society_id, mc.sector): mc + for mc in all_mc + } + ns_ids = set([ + mc.national_society_id + for mc in all_mc + ]) + return [ + ( + mc_by_ns_sector.get((nc_id, sector)) or + MembershipCoordination( + national_society_id=nc_id, + sector=sector, + ) + ) + for nc_id in ns_ids + for sector, _ in MembershipCoordination.Sector.choices + ] + + +class StrategicPriority(models.Model): + class Type(models.TextChoices): + ONGOING_EMERGENCY_OPERATIONS = 'ongoing_emergency_operations', _('Ongoing emergency operations') + CLIMATE_AND_ENVIRONMENTAL_CRISIS = 'climate_and_environmental_crisis', _('Climate and environmental crisis') + EVOLVING_CRISIS_AND_DISASTERS = 'evolving_crisis_and_disasters', _('Evolving crisis and disasters') + GROWING_GAPS_IN_HEALTH_AND_WELLBEING = 'growing_gaps_in_health_and_wellbeing', _('Growing gaps in health and wellbeing') + MIGRATION_AND_IDENTITY = 'migration_and_identity', _('Migration and Identity') + VALUE_POWER_AND_INCLUSION = 'value_power_and_inclusion', _('Value power and inclusion') + + country_plan = models.ForeignKey( + CountryPlan, on_delete=models.CASCADE, + verbose_name=_('Country Plan'), + related_name='country_plan_sp', + ) + type = models.CharField(max_length=100, choices=Type.choices, verbose_name=_('Type')) + funding_requirement = models.FloatField(verbose_name=_('Funding Requirement'), blank=True, null=True) + people_targeted = models.IntegerField(verbose_name=_('People Targeted'), blank=True, null=True) + + class Meta: + unique_together = ('country_plan', 'type') + + def __str__(self): + return f'{self.type}' + + +class MembershipCoordination(models.Model): + class Sector(models.TextChoices): + CLIMATE = 'climate', _('Climate') + CRISIS = 'crisis', _('Crisis') + HEALTH = 'health', _('Health') + MIGRATION = 'migration', _('Migration') + INCLUSION = 'inclusion', _('Inclusion') + ENGAGED = 'engaged', _('Engaged') + ACCOUNTABLE = 'accountable', _('Accountable') + TRUSTED = 'trusted', _('Trusted') + + country_plan = models.ForeignKey( + CountryPlan, + on_delete=models.CASCADE, + verbose_name=_('Country Plan'), + related_name='country_plan_mc', + ) + sector = models.CharField(max_length=100, choices=Sector.choices, verbose_name=_('Sector')) + national_society = models.ForeignKey(Country, on_delete=models.CASCADE, related_name='national_society_mc') + has_coordination = models.BooleanField(default=False) + + class Meta: + unique_together = ('country_plan', 'national_society', 'sector') + + def __str__(self): + return f'{self.national_society.iso3}-{self.sector}' diff --git a/country_plan/serializers.py b/country_plan/serializers.py new file mode 100644 index 000000000..2ce0f5bad --- /dev/null +++ b/country_plan/serializers.py @@ -0,0 +1,69 @@ +from rest_framework import serializers + +from .models import ( + CountryPlan, + StrategicPriority, + MembershipCoordination, +) + + +class StrategicPrioritySerializer(serializers.ModelSerializer): + type_display = serializers.CharField(source='get_type_display', read_only=True) + + class Meta: + model = StrategicPriority + fields = ( + 'id', + 'country_plan', + 'type', + 'type_display', + 'funding_requirement', + 'people_targeted', + ) + + +# NOTE: Defined to be used with CountryPlanViewset +class MembershipCoordinationSerializer(serializers.ModelSerializer): + sector_display = serializers.CharField(source='get_sector_display', read_only=True) + national_society_name = serializers.CharField(read_only=True) + + class Meta: + model = MembershipCoordination + fields = ( + 'id', + 'country_plan', + 'national_society', + 'national_society_name', + 'sector', + 'sector_display', + 'has_coordination', + ) + + +# NOTE: Defined to be used with CountryPlanViewset +class CountryPlanSerializer(serializers.ModelSerializer): + strategic_priorities = StrategicPrioritySerializer(source='country_plan_sp', many=True, read_only=True) + membership_coordinations = MembershipCoordinationSerializer(source='full_country_plan_mc', many=True, read_only=True) + internal_plan_file = serializers.SerializerMethodField() + + class Meta: + model = CountryPlan + fields = ( + 'country', + 'internal_plan_file', + 'public_plan_file', + 'requested_amount', + 'people_targeted', + 'is_publish', + # Manual Defined + 'strategic_priorities', + 'membership_coordinations', + ) + + def get_internal_plan_file(self, obj): + file = obj.internal_plan_file + request = self.context['request'] + if request.user.is_authenticated and file.name: + return request.build_absolute_uri( + serializers.FileField().to_representation(file) + ) diff --git a/country_plan/tasks.py b/country_plan/tasks.py new file mode 100644 index 000000000..228917b65 --- /dev/null +++ b/country_plan/tasks.py @@ -0,0 +1,164 @@ +import re +import logging +import pandas as pd +import operator +from functools import reduce + +from celery import shared_task +from django.db import transaction, models + +from api.models import CountryType +from .models import ( + DataImport, + CountryPlan, + StrategicPriority, + MembershipCoordination +) +from api.models import Country + +logger = logging.getLogger(__name__) + + +class CountryPlanImporter(): + CSV_COLUMN = [ + 'ISO3', + 'Country', + 'Ongoing emergency operations', + 'SP1 - Climate and environmental crisis', + 'SP2 - Evolving crises and disasters', + 'SP3 - Growing gaps in health and wellbeing', + 'SP4 - Migration and identity', + 'SP5 - Values, power, and inclusion', + 'Max of people to be reached ', + 'SP1', + 'SP2', + 'SP3', + 'SP4', + 'SP5', + 'EA1', + 'EA2', + 'EA3', + 'TOTAL\nFUNDING REQUIREMENTS', + ] + + SP_COLUMN = [ + 'SP1', + 'SP2', + 'SP3', + 'SP4', + 'SP5', + 'EA1', + 'EA2', + 'EA3', + ] + + @staticmethod + def _process_number(number): + if number in ('', None): + return None + if type(number) in [float, int]: + return number + # Some specific cases using regex + value_search = re.search(r'(?P(\d+(?:\.\d+)?))?\s?(?P[MKB])', number) + if value_search is None or ( + value_search.group('value') is None or + value_search.group('expression') is None + ): + return pd.to_numeric(number) + value = pd.to_numeric(value_search.group('value')) + expression = value_search.group('expression') + EXPRESSION_MULTIPLIER = { + 'K': 1000, + 'M': 1000000, + 'B': 1000000000, + } + return value * EXPRESSION_MULTIPLIER[expression] + + @classmethod + def _save_country_plan(cls, row): + country = Country.objects.filter(record_type=CountryType.COUNTRY).get(iso3=row.ISO3) + # -- Country Plan + country_plan, _ = CountryPlan.objects.get_or_create(country=country) + country_plan.requested_amount = cls._process_number(row._18) + country_plan.people_targeted = cls._process_number(row._9) + country_plan.save() + + # -- StrategicPriority + target_values_list = [row._3, row._4, row._5, row._6, row._7, row._8] + sp_target_value_map = dict(zip(StrategicPriority.Type, target_values_list)) + for sp, t_people in sp_target_value_map.items(): + strategic_priority, _ = StrategicPriority.objects.get_or_create( + country_plan=country_plan, + type=sp, + ) + strategic_priority.people_targeted = cls._process_number(t_people) + # strategic_priority.funding_requirement? + strategic_priority.save() + + # -- MembershipCoordination + n_society_list = [row.SP1, row.SP2, row.SP3, row.SP4, row.SP5, row.EA1, row.EA2, row.EA3] + sp_column_n_society_map = dict(zip(cls.SP_COLUMN, n_society_list)) + sp_column_ns_type_map = dict(zip(cls.SP_COLUMN, MembershipCoordination.Sector)) + membership_coordination_ids = [] + for sp in cls.SP_COLUMN: + national_societies_str = set([ + name.strip() + for name in str(sp_column_n_society_map[sp] or '').split(',') + if name.strip() + ]) + if not national_societies_str: + continue + countries_qs = Country.objects.filter( + reduce( + operator.or_, ( + models.Q(society_name__icontains=name) + for name in national_societies_str + ) + ), + record_type=CountryType.COUNTRY, + in_search=True, + ) + # TODO: Better error handling + missing_ns_in_db = set(national_societies_str) - set(countries_qs.values_list('society_name', flat=True)) + if missing_ns_in_db: + raise Exception(f"Missing NS in the database: {missing_ns_in_db}") + for country in countries_qs.all(): + membership_coordination, _ = MembershipCoordination.objects.get_or_create( + country_plan=country_plan, + national_society=country, + sector=sp_column_ns_type_map[sp], + ) + membership_coordination.has_coordination = True + membership_coordination.save() + membership_coordination_ids.append(membership_coordination.id) + # Update dangling membership coordination + MembershipCoordination.objects\ + .filter(country_plan=country_plan)\ + .exclude(id__in=membership_coordination_ids)\ + .update(has_coordination=False) + + @classmethod + def process(cls, file): + data = pd.read_excel(file, skiprows=1, na_filter=False, usecols=cls.CSV_COLUMN) + data = data[:-1] + + errors = [] + for row in data.itertuples(): + try: + with transaction.atomic(): + cls._save_country_plan(row) + except Exception as e: + logger.error(f'Failed to import Country-Plan: iso3:{row.ISO3}', exc_info=True) + errors.append(dict( + iso3=row.ISO3, + error=str(e), + )) + return errors + + +@shared_task +def process_data_import(pk): + data_import = DataImport.objects.get(pk=pk) + errors = CountryPlanImporter.process(data_import.file) + data_import.errors = errors + data_import.save() diff --git a/country_plan/tests/__init__.py b/country_plan/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/country_plan/tests/test_commands.py b/country_plan/tests/test_commands.py new file mode 100644 index 000000000..1c336c11d --- /dev/null +++ b/country_plan/tests/test_commands.py @@ -0,0 +1,169 @@ +from unittest import mock +from django.core.management import call_command + +from main.test_case import APITestCase +from api.factories.country import CountryFactory +from country_plan.factories import CountryPlanFactory +from country_plan.models import CountryPlan +from country_plan.management.commands.ingest_country_plan_file import PUBLIC_SOURCE, INTERNAL_SOURCE + +# NOTE: Only defined used fields +FILE_BASE_DIRECTORY = 'https://example.org/Download.aspx?FileId=' +PUBLIC_APPEAL_COUNTRY_PLAN_MOCK_RESPONSE = [ + { + 'BaseDirectory': FILE_BASE_DIRECTORY, + 'BaseFileName': '000000', + 'Inserted': '2022-11-29T11:24:00', + 'LocationCountryCode': 'SY', + 'LocationCountryName': 'Syrian Arab Republic' + }, + { + 'BaseDirectory': FILE_BASE_DIRECTORY, + 'BaseFileName': '000001', + 'Inserted': '2022-11-29T11:24:00', + 'LocationCountryCode': 'CD', + 'LocationCountryName': 'Congo, The Democratic Republic Of The' + }, + # Not included in INTERNAL + { + 'BaseDirectory': FILE_BASE_DIRECTORY, + 'BaseFileName': '000002', + 'Inserted': '2022-11-29T11:24:00', + 'LocationCountryCode': 'MM', + 'LocationCountryName': 'Myanmar' + }, + { + 'BaseDirectory': FILE_BASE_DIRECTORY, + 'BaseFileName': '000003', + 'Inserted': '2022-11-29T11:24:00', + 'LocationCountryCode': 'TM', + 'LocationCountryName': 'Turkmenistan' + }, + { + 'BaseDirectory': FILE_BASE_DIRECTORY, + 'BaseFileName': '000004', + 'Inserted': '2022-11-29T11:24:00', + 'LocationCountryCode': 'GR', + 'LocationCountryName': 'Greece' + }, + { + 'BaseDirectory': FILE_BASE_DIRECTORY, + 'BaseFileName': 'NOOP', + 'Inserted': '2022-11-29T11:24:00', + 'LocationCountryCode': 'XY', + 'LocationCountryName': 'Myanmar' + }, +] + +INTERNAL_APPEAL_COUNTRY_PLAN_MOCK_RESPONSE = [ + { + 'BaseDirectory': FILE_BASE_DIRECTORY, + 'BaseFileName': '000000', + 'Inserted': '2022-11-29T11:24:00', + 'LocationCountryCode': 'SY', + 'LocationCountryName': 'Syrian Arab Republic' + }, + { + 'BaseDirectory': FILE_BASE_DIRECTORY, + 'BaseFileName': '000001', + 'Inserted': '2022-11-29T11:24:00', + 'LocationCountryCode': 'CD', + 'LocationCountryName': 'Congo, The Democratic Republic Of The' + }, + # Not included in PUBLIC + { + 'BaseDirectory': FILE_BASE_DIRECTORY, + 'BaseFileName': '000001', + 'Inserted': '2022-11-29T11:24:00', + 'LocationCountryCode': 'NP', + 'LocationCountryName': 'Nepal' + }, +] + + +class MockResponse(): + class FileStream(): + def __init__(self, stream): + self.stream = stream + + def raise_for_status(self): + pass + + def iter_content(self, **_): + return self.stream + + def __init__(self, json=None, stream=None, headers=None): + self._headers = headers + self._json = json + self.stream = stream + + def json(self): + return self._json + + @property + def headers(self): + return self._headers + + def __enter__(self): + return MockResponse.FileStream(self.stream) + + def __exit__(self, *_): + pass + + +class CountryPlanIngestTest(APITestCase): + def mock_request(url, *_, **kwargs): + if url == PUBLIC_SOURCE: + return MockResponse(json=PUBLIC_APPEAL_COUNTRY_PLAN_MOCK_RESPONSE) + if url == INTERNAL_SOURCE: + return MockResponse(json=INTERNAL_APPEAL_COUNTRY_PLAN_MOCK_RESPONSE) + if url.startswith(FILE_BASE_DIRECTORY): + return MockResponse(stream=[b'']) + + def mock_request_head(url, *_, **kwargs): + headers = { + 'Content-Type': 'application/pdf', + 'Content-Disposition': 'attachment;filename=Sample_document_2023.pdf', + } + if url.endswith('NOOP'): + headers['Content-Type'] = 'html/text' + elif url.endswith('000004'): + headers['Content-Disposition'] = '' + return MockResponse(headers=headers) + + @mock.patch('country_plan.management.commands.ingest_country_plan_file.requests.head', side_effect=mock_request_head) + @mock.patch('country_plan.management.commands.ingest_country_plan_file.requests.get', side_effect=mock_request) + @mock.patch('main.utils.requests.get', side_effect=mock_request) + def test_country_plan_ingest(self, *_): + CountryPlanFactory.create( + country=CountryFactory.create( + name='Random Country', + iso='RC', + ), + ) + EXISTING_CP = 1 + for country_iso in set([ + item['LocationCountryCode'] + for item in [ + *PUBLIC_APPEAL_COUNTRY_PLAN_MOCK_RESPONSE, + *INTERNAL_APPEAL_COUNTRY_PLAN_MOCK_RESPONSE, + ] + ]): + if country_iso == 'CD': + # Not create country for this + continue + CountryFactory.create(iso=country_iso) + # Before + assert CountryPlan.objects.count() == EXISTING_CP + assert CountryPlan.objects.filter(is_publish=True).count() == 0 + assert CountryPlan.objects.exclude(public_plan_file='').count() == 0 + call_command('ingest_country_plan_file') + # After + assert CountryPlan.objects.count() == EXISTING_CP + 5 + assert CountryPlan.objects.filter(is_publish=True).count() == 5 + assert CountryPlan.objects.exclude(public_plan_file='').count() == 4 + assert CountryPlan.objects.exclude(internal_plan_file='').count() == 2 + # First downloaded document. Others will have Sample_document_2023{random-chars}.pdf + assert CountryPlan.objects.filter(country__iso='SY').first().public_plan_file.name.endswith('Sample_document_2023.pdf') + # Without attachment filename + assert CountryPlan.objects.filter(country__iso='GR').first().public_plan_file.name.endswith('document.pdf') diff --git a/deployments/migrations/0070_alter_personnel_options.py b/deployments/migrations/0070_alter_personnel_options.py new file mode 100644 index 000000000..9a2ca4fbe --- /dev/null +++ b/deployments/migrations/0070_alter_personnel_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.16 on 2023-01-17 10:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('deployments', '0069_auto_20221022_1542'), + ] + + operations = [ + migrations.AlterModelOptions( + name='personnel', + options={'ordering': ('deployment', 'country_to', 'country_from', 'molnix_id', 'deployedperson_ptr_id'), 'verbose_name': 'Personnel', 'verbose_name_plural': 'Personnels'}, + ), + ] diff --git a/deployments/models.py b/deployments/models.py index 0c1f8f3d0..50623dbc2 100644 --- a/deployments/models.py +++ b/deployments/models.py @@ -9,11 +9,9 @@ from django.utils.hashable import make_hashable from django.utils.encoding import force_str from django.contrib.postgres.fields import ArrayField -from django.contrib.gis.db import models as gid_models from django.db.models import Q from django.db.models import JSONField -from main.enums import TextChoices from api.models import ( District, Country, @@ -212,6 +210,7 @@ def __str__(self): return '%s: %s - %s' % (self.type.upper(), self.name, self.role) class Meta: + ordering = ('deployment', 'country_to', 'country_from', 'molnix_id', 'deployedperson_ptr_id',) verbose_name = _('Personnel') verbose_name_plural = _('Personnels') diff --git a/deployments/search_indexes.py b/deployments/search_indexes.py new file mode 100644 index 000000000..16583f0bd --- /dev/null +++ b/deployments/search_indexes.py @@ -0,0 +1,45 @@ +from haystack import indexes + +from deployments.models import Project, ERU + + +class ProjectIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.EdgeNgramField(document=True, use_template=True) + name = indexes.EdgeNgramField(model_attr='name') + event_name = indexes.CharField(model_attr='event__name') + start_date = indexes.DateTimeField(model_attr='start_date', null=True) + end_date = indexes.DateTimeField(model_attr='end_date', null=True) + reporting_ns = indexes.CharField(model_attr='reporting_ns__name') + project_districts = indexes.MultiValueField() + sector = indexes.CharField(model_attr='get_primary_sector_display') + tags = indexes.MultiValueField(model_attr='get_secondary_sectors_display') + target_total = indexes.IntegerField(model_attr='target_total', null=True) + event_id = indexes.IntegerField(model_attr='event__id', null=True) + reporting_ns_id = indexes.IntegerField(model_attr='reporting_ns__id') + + def get_model(self): + return Project + + def prepare_project_districts(self, obj): + return [district.name for district in obj.project_districts.all()] + + def index_queryset(self, using=None): + return self.get_model().objects.all() + + +class ERUIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.EdgeNgramField(document=True, use_template=True) + event_name = indexes.EdgeNgramField(model_attr='event__name') + country = indexes.CharField(model_attr='deployed_to__name') + personnel_units = indexes.IntegerField(model_attr='units', null=True) + equipment_units = indexes.IntegerField(model_attr='equipment_units', null=True) + eru_type = indexes.CharField(model_attr='get_type_display') + eru_owner = indexes.CharField(model_attr='eru_owner__national_society_country__society_name') + event_id = indexes.IntegerField(model_attr='event__id', null=True) + country_id = indexes.IntegerField(model_attr='deployed_to__id') + + def get_model(self): + return ERU + + def index_queryset(self, using=None): + return self.get_model().objects.all() \ No newline at end of file diff --git a/deployments/snapshots/snap_tests.py b/deployments/snapshots/snap_tests.py index e369faf99..62a375407 100644 --- a/deployments/snapshots/snap_tests.py +++ b/deployments/snapshots/snap_tests.py @@ -17,14 +17,14 @@ 0,6150000,,,21,21,disaster-type-pdZDbQAvNqHbuogyciTiCOtppdnCRlZPTWFeIAOPkstDwAmeZZ,giOenISaFIlJFIWZdzAeuStwNGvGzyowBuTsfHbkOJRHlviekQHTIyBLcuNryLTMHWtlIGkSIsCcvjcpfQoRkNIJDfYUWknVkbnByHBsiYCVEIyiBISQXvlEjHDVybUpjaGECJorCfVaAlQIoorOOwWWOTxEpvBlmPiZCFPTXcdqvnvHwTLEndiXDoVOQikJwZCbtTkYqcWUjvvvsAHMUYSRoLYCDPcsggAEJIexYLAOYszPDoHzYvyMatrGQqVQBFVonlTlNeSksIMvwIDSCbaQBkpRNquLLRrkArcFAbOJMundfiTdopKGbShpUGFfWyjIopwBJNduJXecIRbhxnnDrZmuzDbiOPCFkGXDeuyxMBQNxDSLQswFvKvNKxxbvuPpSOyiKTOfChtGxseJoNwkSuiVQxjZdDQXHPGkXWezPugeNOTlftxFsTsujdZncYZQiEOyWNqDmbGUXJjtdmUxRplUfYkVssaVSlLmXBosPYYbKqfl,2008-01-01,14,19,,14,event-YBhVAieQQIQxTBpuiaXaDBeucsjduMJQyGZwQPAMCDetmRnSBb,13,gcvwlcgcfswwaugdunmjfghgjgimncrzzqhypjhhagetiuwfhr,7,2008-01-01T00:00:00.123456Z,,,project-ZTfJcxTQkwEKuLKdTbsEMqfiZPpjutAafJMfYhnZtVUoqZGTxE,0,Programme,3,Migration,0,Bilateral,15,,,15,,False,tk,FOt,country-QJXYLBCzRpwmYBFpxRYwTHMAcFdDiiIInrXfnoZwgWmNaermdM,4,Country Office,15,society-name-NoBzRouETXUKACXeSrHbmTXQaQdDRUoZxXOBxJjybGlvlQApUE,"gRoEbNJuNo, nPOMbdYvpi","1, 2","False, True","True, True","district-uVAlmiYIxHGrkqEZsVvZhDejWoRURzZJxfYzaqIhDxRVRqLyOx, district-cctFhgpIiyDxQVSIALVUjAPgFNArcSxnCCpxgRxIPwdzrmhDfQ",0,0,0,0,7,2008-01-01T00:00:00.123456Z,7,2008-01-01T00:00:00.123456Z,regional-project-sihXhjbXkNfSTnGfocfsLtJckNHMdJIPHTeLyRAtZxkmhKgRKL,14,,,,,,14,,False,dn,yuA,country-pHPsKpWVAJdQymuGwobUfbXfWjCJKyMvfZNPnaWsPGxjTFDwHy,4,Country Office,14,society-name-jdmVBnqtRsnOMqzGXMyVVTpogIeRjmoxCRAfETVdCohCtrwDDB,"0, 1","WASH, PGI",2008-01-01,1,Ongoing,0,0,0,0,8,public,Public\r 0,9070000,,,24,24,disaster-type-EdbprpWDrmUjwyZREsbYYSvnyiufPQjnhevqgOehLbOibyaFgp,VzPLndYcxileRtqkPBYCPpVWxmJqQiuXBhhAgRuQWNKTNtrVjAYSfIrvunCWXuIjzHiUPNxdIgrNLQtclZmimwJxLowGdSnQVOvPkGHbvbZFucFTQGfLROCZnxqYYsiwlrTSEFFEqsXrKyqYLpctBsZddBayKIGiytIBiVQDxZxZnFDLoYJwPsPZybLgihbqbDkxqZwUmMBcfWBdKNojOVqDbCKjzixQYgTEsmhRzXdnixdPoVRSXLtpROMGHqrjDJKwjRpBfYFLqKAUqvZtMSVwTNwDUGhlCUmUgFTsRUQZjCUgHoijRrbknMnzQiEygDArTbXQrcrUyvQxgfkJyoGfhbwaiTZiITMkcEXPsROvLwkYHTiCXAFEYrlnNnuSqnWoODmUYiTCHnMAXVLlfwhcaiyaFCWkeqmOOqSHyKQYyYvnFexwEphbwlaJKJmeJDobQZKxdENnoDCogiNEmrfVtHvdXkRSDQxMOSbHjAITbaMdEjlb,2008-01-01,16,22,,16,event-DnBvQsSRasqNiElGdNamhCJGpYQYyZqpVfBFVHuVNGrqrcJJQK,15,uvlnbmlocdoxbatvjbnnxdhvxknpcimmqnzymobmkpwzwqapbc,8,2008-01-01T00:00:00.123456Z,,,project-JPOEfkWhHLWrwlpMPSuKAXcipneuZNXNHUDwhDxCYNbmkmaJHI,0,Programme,6,Shelter,0,Bilateral,17,,,17,,False,oK,AIC,country-bkLvYEmYHagbelsZwDgIfxJdyVPZfMsMKXLhmzOYUxcwvhKgQV,5,Representative Office,17,society-name-fGBYQFeEaIaPtNjMnTkWJTYxbFSgMBfjLonieWmrKwkicKXZqc,"gRoEbNJuNo, nPOMbdYvpi","1, 2","False, True","True, True","district-uVAlmiYIxHGrkqEZsVvZhDejWoRURzZJxfYzaqIhDxRVRqLyOx, district-cctFhgpIiyDxQVSIALVUjAPgFNArcSxnCCpxgRxIPwdzrmhDfQ",0,0,0,0,8,2008-01-01T00:00:00.123456Z,8,2008-01-01T00:00:00.123456Z,regional-project-dCGOIBFPwHSYFuvRIvaLvKWgtvcsNClpzANsRziClGBvgIhsXS,16,,,,,,16,,False,Lx,BbF,country-muzJUSngCjoWBOzFdtTSeEfaPnSJKfNhGYWEscubhpUnYkbWpr,3,Region,16,society-name-eqHEXzwdTFDwWTjwTPDjoeyWVnhzbmQFPGUefPQVXduXSWCbmW,"0, 1","WASH, PGI",2008-01-01,1,Ongoing,0,0,0,0,9,public,Public\r 0,6280000,,,27,27,disaster-type-zCgOSYQZaQGaaTqOwGSElrkQlERPsUSLzBwTQiNtHmcwhDhLDj,VvelPnqnWpuoBDxRCREFCnqUYiyZXvIEudnQeatTPLCMfqaEEeOROlGbYAfQKMcEqzzTAMWFQrEjYVfDhWksXoxyBGQZpYVdnjwWmyKxTjxuICUVyFnuUaEJWfVdQFlHSoPMtZDyrUAVRelRyfqNSOYMnQzQpgXKzlCvzStqoLWcLueOMvzznbdPzmyjThYdwReyqdCGDVZTPdtGgfnVPxWWmSUEVOQOpMAxkEOkWZMuLVIzCDESdvxtHLLTXcEflSLMyMWMgusTJlEadQiKpnXSgnizGKmEqAbkepBWIIsexFEfSiVMeHrlSOPxeULwYAWfEBQxvJMkjoSxxiSIuJFQbBmaYQjXjJXTlMoZpwjICHNFgxUmwZMPAtFkAHQtVFrArRxucDRjHpgWahJwURdHIdIEuzVtwWbVRvIhPauHagaJPJRAFMFZqqFBfLnCIZXUJBymyYOsyWvpootCCrVKAomEKjjYSBkGhNewalnFsnJOFpsu,2008-01-01,18,25,,18,event-taTcnVkEnbnWitXHlTCtSynVYuiRUoYvAfhJGSWjapIaRBRTUL,17,vgqaiqejvmpqetggpkgwfdfymbznozbyzozpuntelgeppkaulg,9,2008-01-01T00:00:00.123456Z,,,project-dukhMsauOaqEiWAbNEPKBkMerwLFCpAybKIWVuLhMJfCWtFUoy,0,Programme,5,DRR,1,Multilateral,19,,,19,,False,ER,Zvn,country-kHVnkyFRbgVWfzCGhhZBDYVIAViMalugvmGzqWkBCoXbkkcNKt,1,Country,19,society-name-PbsRaxYmeMHrXedQvbCuYuaHqGukJsAFCdoYFGqBCNFdUaJPUo,"gRoEbNJuNo, nPOMbdYvpi","1, 2","False, True","True, True","district-uVAlmiYIxHGrkqEZsVvZhDejWoRURzZJxfYzaqIhDxRVRqLyOx, district-cctFhgpIiyDxQVSIALVUjAPgFNArcSxnCCpxgRxIPwdzrmhDfQ",0,0,0,0,9,2008-01-01T00:00:00.123456Z,9,2008-01-01T00:00:00.123456Z,regional-project-hZONIqqqYUMyeBpKulvEqEEErpCOkfjoGrGDCxyeYNfJWKHEvx,18,,,,,,18,,False,dz,PZB,country-JrWQucRSqzjgFyqQmsMmKsgBYTWyCqcCJRJRSAKGVSxIuCrNeW,1,Country,18,society-name-DPjBsfGCNpPLCINunDEdekrKnhBVrwRQMHjqpCIarCascEOLmy,"0, 1","WASH, PGI",2008-01-01,1,Ongoing,0,0,0,0,10,public,Public\r -0,3970000,,,30,30,disaster-type-YZgWdpAPmOaxyMvPPWAluOcRLyNteinukJHINLRYaRJGWWHyWb,HefaDgsMVlnTzbpXAGbGPvJQGsqYXrnZFiXhmlBYTTFFAnJYoXeTLeRGdZtoGtSpcXLEKFvXyGTYqlhJhJWceZHVErDkMfTSoghkywNSCtyandiRngieFHhCYWbtlTpiFuHIJffIuNZkbAnbqhywiDkzfJKgaKwogcbeDMQlWueozOkjmIPbenQCclbKPJfMtaeWryNyfTpzcDFjFkcOVsxdSTKJaBVnSRwopnxbhzTlAiuectKryhFpcxQeZUvgnoQibLzCmdLYjsaEtfOmvKORAAvppKdoYyoQHxErmoMgLuGbSabYJtgzjRyFxcfTtBHpPhrzxaYboKqXOxxGTRUTsyVsPkdgGgWPMbinkagywgMHJMazVbMhFXwcDvhVLkyDEOwZbrzgzPkQjROGOsmUMRBwBIVOJWOFYkajaqJFfboyYRvosFyWsfqsYjUTVEhRLCsvnesxLxwaJddjONbBULwtEmuBqiZCSiLqnAOfcYNbKHkq,2008-01-01,20,28,,20,event-uFoeQGdjbsbqeTaDZgKeiMXZrpYSpDdlBxCHgsNzoZwubfLnxn,19,eqfbhuugtxzcofoedscusteqsdrkmfcxabzfoalgtscndnpmez,10,2008-01-01T00:00:00.123456Z,,,project-udPHhqTBQtCcjmAaCshFdYBaklxkvumQQfmqVfCAllIbKwteFx,0,Programme,0,WASH,1,Multilateral,21,,,21,,False,sB,qum,country-hqEusQzBjrWHjiodPBsiVeYfBGqzdoHZMZwgOSvNjprSBzujeW,4,Country Office,21,society-name-WogBWrLYuaBcXuPsggSnEPgoPRsPBVYpOgWRaFIMRcOEIBKRVu,"gRoEbNJuNo, nPOMbdYvpi","1, 2","False, True","True, True","district-uVAlmiYIxHGrkqEZsVvZhDejWoRURzZJxfYzaqIhDxRVRqLyOx, district-cctFhgpIiyDxQVSIALVUjAPgFNArcSxnCCpxgRxIPwdzrmhDfQ",0,0,0,0,10,2008-01-01T00:00:00.123456Z,10,2008-01-01T00:00:00.123456Z,regional-project-nWNVzNOgbWWIkZLBPQwBgiZdYysyrUAEyqDJwykvdJqwdVRePt,20,,,,,,20,,False,pm,xYm,country-iUlMcOXbduLauswVhCoyuJxscuUKeKortlsAiQVEgimNopdXWA,3,Region,20,society-name-TxNEuLQIbKzZkGlUThMzaHFJFWOuOEGPJjXSYEmDBpdLEDoLPG,"0, 1","WASH, PGI",2008-01-01,1,Ongoing,0,0,0,0,11,public,Public\r +0,980000,,,30,30,disaster-type-FYZgWdpAPmOaxyMvPPWAluOcRLyNteinukJHINLRYaRJGWWHyW,bHefaDgsMVlnTzbpXAGbGPvJQGsqYXrnZFiXhmlBYTTFFAnJYoXeTLeRGdZtoGtSpcXLEKFvXyGTYqlhJhJWceZHVErDkMfTSoghkywNSCtyandiRngieFHhCYWbtlTpiFuHIJffIuNZkbAnbqhywiDkzfJKgaKwogcbeDMQlWueozOkjmIPbenQCclbKPJfMtaeWryNyfTpzcDFjFkcOVsxdSTKJaBVnSRwopnxbhzTlAiuectKryhFpcxQeZUvgnoQibLzCmdLYjsaEtfOmvKORAAvppKdoYyoQHxErmoMgLuGbSabYJtgzjRyFxcfTtBHpPhrzxaYboKqXOxxGTRUTsyVsPkdgGgWPMbinkagywgMHJMazVbMhFXwcDvhVLkyDEOwZbrzgzPkQjROGOsmUMRBwBIVOJWOFYkajaqJFfboyYRvosFyWsfqsYjUTVEhRLCsvnesxLxwaJddjONbBULwtEmuBqiZCSiLqnAOfcYNbKHk,2008-01-01,20,28,,20,event-gVuFoeQGdjbsbqeTaDZgKeiMXZrpYSpDdlBxCHgsNzoZwubfLn,19,xneqfbhuugtxzcofoedscusteqsdrkmfcxabzfoalgtscndnpm,10,2008-01-01T00:00:00.123456Z,,,project-qudPHhqTBQtCcjmAaCshFdYBaklxkvumQQfmqVfCAllIbKwteF,0,Programme,4,Health,1,Multilateral,21,,,21,,False,je,WFs,country-tQTehqEusQzBjrWHjiodPBsiVeYfBGqzdoHZMZwgOSvNjprSBz,3,Region,21,society-name-BqumWogBWrLYuaBcXuPsggSnEPgoPRsPBVYpOgWRaFIMRcOEIB,"gRoEbNJuNo, nPOMbdYvpi","1, 2","False, True","True, True","district-uVAlmiYIxHGrkqEZsVvZhDejWoRURzZJxfYzaqIhDxRVRqLyOx, district-cctFhgpIiyDxQVSIALVUjAPgFNArcSxnCCpxgRxIPwdzrmhDfQ",0,0,0,0,10,2008-01-01T00:00:00.123456Z,10,2008-01-01T00:00:00.123456Z,regional-project-NnnWNVzNOgbWWIkZLBPQwBgiZdYysyrUAEyqDJwykvdJqwdVRe,20,,,,,,20,,False,vp,mxY,country-oiUlMcOXbduLauswVhCoyuJxscuUKeKortlsAiQVEgimNopdXW,4,Country Office,20,society-name-mTxNEuLQIbKzZkGlUThMzaHFJFWOuOEGPJjXSYEmDBpdLEDoLP,"0, 1","WASH, PGI",2008-01-01,1,Ongoing,0,0,0,0,11,public,Public\r ''' snapshots['TestProjectAPI::test_global_project_api 1'] = { 'ns_with_ongoing_activities': 16, 'projects_per_programme_type': [ { - 'count': 8, + 'count': 7, 'programme_type': 0, 'programme_type_display': 'Bilateral' }, @@ -34,7 +34,7 @@ 'programme_type_display': 'Multilateral' }, { - 'count': 4, + 'count': 5, 'programme_type': 2, 'programme_type_display': 'Domestic' } @@ -88,12 +88,17 @@ 'primary_sector_display': 'CEA' }, { - 'count': 2, + 'count': 1, 'primary_sector': 3, 'primary_sector_display': 'Migration' }, { 'count': 2, + 'primary_sector': 4, + 'primary_sector_display': 'Health' + }, + { + 'count': 1, 'primary_sector': 5, 'primary_sector_display': 'DRR' }, @@ -303,8 +308,8 @@ { 'budget_amount_total': 5920000, 'id': 21, - 'iso3': 'WNq', - 'name': 'country-FWJNPIGxPkqKRLeNzbOHsgUSwRJXvxmDTodTYrnDAnhHuxKzFb', + 'iso3': 'hBW', + 'name': 'country-hkFWJNPIGxPkqKRLeNzbOHsgUSwRJXvxmDTodTYrnDAnhHuxKz', 'ongoing_projects': 1, 'operation_types': [ 0 @@ -319,7 +324,7 @@ 'primary_sector_display': 'CEA' } ], - 'society_name': 'society-name-qJTgrVtaadVUrcYCxAcjpXhdUBtXcCXMFkLAHVklUJGQvVYtjD', + 'society_name': 'society-name-NqqJTgrVtaadVUrcYCxAcjpXhdUBtXcCXMFkLAHVklUJGQvVYt', 'target_total': 0 }, { @@ -411,7 +416,7 @@ 'target_total': 0 }, { - 'budget_amount_total': 6730000, + 'budget_amount_total': 7050000, 'id': 31, 'iso3': 'IBB', 'name': 'country-TtOYzaCmFldtcpOlRYtastbgkeCJvBTOnPWtmzervxcfkMbqpf', @@ -433,10 +438,10 @@ 'target_total': 0 }, { - 'budget_amount_total': 5220000, + 'budget_amount_total': 4260000, 'id': 33, - 'iso3': 'hNK', - 'name': 'country-lFoucligTrYZNfzfdSWfMLvoRjfavNEGPtQYlQIfZsPpOuhTPN', + 'iso3': 'PNP', + 'name': 'country-TIuGhslFoucligTrYZNfzfdSWfMLvoRjfavNEGPtQYlQIfZsPp', 'ongoing_projects': 1, 'operation_types': [ 1 @@ -447,18 +452,18 @@ 'projects_per_sector': [ { 'count': 1, - 'primary_sector': 5, - 'primary_sector_display': 'DRR' + 'primary_sector': 4, + 'primary_sector_display': 'Health' } ], - 'society_name': 'society-name-DMYIfFApbQgMPVWggwrZnUhBSCdfQshatRJHibAqQwyjPObObN', + 'society_name': 'society-name-tSThNKDMYIfFApbQgMPVWggwrZnUhBSCdfQshatRJHibAqQwyj', 'target_total': 0 }, { - 'budget_amount_total': 3510000, + 'budget_amount_total': 3040000, 'id': 35, - 'iso3': 'Bnz', - 'name': 'country-NUBFklsXHvwhvOCcmqmsYKfCZeQddoPOvFOcvIoPoPYtLrpOFs', + 'iso3': 'PYt', + 'name': 'country-fdGdNQeaEPjKNUBFklsXHvwhvOCcmqmsYKfCZeQddoPOvFOcvI', 'ongoing_projects': 1, 'operation_types': [ 1 @@ -469,27 +474,27 @@ 'projects_per_sector': [ { 'count': 1, - 'primary_sector': 3, - 'primary_sector_display': 'Migration' + 'primary_sector': 4, + 'primary_sector_display': 'Health' } ], - 'society_name': 'society-name-JvnKNSlGzUnyLPRoqRfeZxiwArYgPzwdBsnTobwsdWUzKBxgrG', + 'society_name': 'society-name-LrpOFsFxdBnzJvnKNSlGzUnyLPRoqRfeZxiwArYgPzwdBsnTob', 'target_total': 0 } ] } snapshots['TestProjectAPI::test_personnel_csv_api 1'] = '''event_id,event_glide_id,event_name,event_ifrc_severity_level,event_disaster_type,event_country_name,event_country_iso3,event_country_nationalsociety,event_country_regionname,role,type,name,deployed_id,deployed_to_name,deployed_to_iso3,deployed_to_nationalsociety,deployed_to_regionname,deployed_from_name,deployed_from_iso3,deployed_from_nationalsociety,deployed_from_regionname,start_date,end_date,ongoing,is_active,molnix_id,molnix_sector,molnix_role_profile,molnix_language,molnix_region,molnix_scope,molnix_modality,molnix_operation\r -,,,,,,,,,,,,10,,,,,,,,,,,True,True,,,,,,,,\r -,,,,,,,,,,,,9,,,,,,,,,,,True,True,,,,,,,,\r -,,,,,,,,,,,,8,,,,,,,,,,,True,True,,,,,,,,\r -,,,,,,,,,,,,7,,,,,,,,,,,True,True,,,,,,,,\r -,,,,,,,,,,,,6,,,,,,,,,,,True,True,,,,,,,,\r -,,,,,,,,,,,,5,,,,,,,,,,,True,True,,,,,,,,\r -,,,,,,,,,,,,4,,,,,,,,,,,True,True,,,,,,,,\r -,,,,,,,,,,,,3,,,,,,,,,,,True,True,,,,,,,,\r -,,,,,,,,,,,,2,,,,,,,,,,,True,True,,,,,,,,\r ,,,,,,,,,,,,1,,,,,,,,,,,True,True,,,,,,,,\r +,,,,,,,,,,,,2,,,,,,,,,,,True,True,,,,,,,,\r +,,,,,,,,,,,,3,,,,,,,,,,,True,True,,,,,,,,\r +,,,,,,,,,,,,4,,,,,,,,,,,True,True,,,,,,,,\r +,,,,,,,,,,,,5,,,,,,,,,,,True,True,,,,,,,,\r +,,,,,,,,,,,,6,,,,,,,,,,,True,True,,,,,,,,\r +,,,,,,,,,,,,7,,,,,,,,,,,True,True,,,,,,,,\r +,,,,,,,,,,,,8,,,,,,,,,,,True,True,,,,,,,,\r +,,,,,,,,,,,,9,,,,,,,,,,,True,True,,,,,,,,\r +,,,,,,,,,,,,10,,,,,,,,,,,True,True,,,,,,,,\r ''' snapshots['TestProjectAPI::test_project_create 1'] = { diff --git a/deployments/templates/search/indexes/deployments/eru_text.txt b/deployments/templates/search/indexes/deployments/eru_text.txt new file mode 100644 index 000000000..252423db6 --- /dev/null +++ b/deployments/templates/search/indexes/deployments/eru_text.txt @@ -0,0 +1 @@ +{{object.event_name}} \ No newline at end of file diff --git a/deployments/templates/search/indexes/deployments/project_text.txt b/deployments/templates/search/indexes/deployments/project_text.txt new file mode 100644 index 000000000..3b21393f5 --- /dev/null +++ b/deployments/templates/search/indexes/deployments/project_text.txt @@ -0,0 +1,9 @@ +{{object.name}} +{{object.event_name}} +{{object.reporting_ns}} +{{object.start_date}} +{{object.sector}} +{{obj.get_secondary_sectors_display}} +{% for district in obj.project_districts.all %} + {{ district.name}} +{% endfor %} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 0d46ae9a6..107b75efb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,14 @@ x-server: &base_server_setup ERP_API_ENDPOINT: ${ERP_API_ENDPOINT:-https://ifrctintapim001.azure-api.net/GoAPI/ExtractGoEmergency} ERP_API_SUBSCRIPTION_KEY: ${ERP_API_SUBSCRIPTION_KEY:-abcdef} CELERY_REDIS_URL: ${CELERY_REDIS_URL:-redis://redis:6379/0} + ELASTIC_SEARCH_HOST: ${ELASTIC_SEARCH_HOST:-elasticsearch://elasticsearch:9200} + # Appeal API + APPEALS_USER: ${APPEALS_USER} + APPEALS_PASS: ${APPEALS_PASS} + # Sentry + SENTRY_DSN: ${SENTRY_DSN} + SENTRY_SAMPLE_RATE: ${SENTRY_SAMPLE_RATE:-0.2} + env_file: - .env volumes: @@ -31,6 +39,8 @@ x-server: &base_server_setup depends_on: - db - redis + - elasticsearch + - kibana services: @@ -48,6 +58,39 @@ services: volumes: - redis-data:/data + # NOTE: Used Only for local development + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:7.0.0 + container_name: elasticsearch + environment: + - cluster.name=docker-cluster + - bootstrap.memory_lock=true + - "ES_JAVA_OPTS=-Xms512m -Xmx512m" + - http.cors.enabled=true + - http.cors.allow-origin=* + - discovery.type=single-node + ulimits: + memlock: + soft: -1 + hard: -1 + volumes: + - elastic-search-data:/usr/share/elasticsearch/data + ports: + - 9200:9200 + + # NOTE: Used Only for local development + kibana: + image: 'docker.elastic.co/kibana/kibana:7.0.0' + container_name: kibana + environment: + SERVER_NAME: kibana.local + ELASTICSEARCH_URL: http://elasticsearch:9200 + ports: + - '5601:5601' + depends_on: + - elasticsearch + + serve: <<: *base_server_setup ports: @@ -149,3 +192,4 @@ services: volumes: redis-data: + elastic-search-data: diff --git a/dref/admin.py b/dref/admin.py index 50a703439..59f6afa1a 100644 --- a/dref/admin.py +++ b/dref/admin.py @@ -12,63 +12,68 @@ @admin.register(DrefFile) class DrefFileAdmin(admin.ModelAdmin): - search_fields = ('file',) + search_fields = ("file",) @admin.register(Dref) class DrefAdmin(CompareVersionAdmin, TranslationAdmin, admin.ModelAdmin): - search_fields = ('title',) - list_display = ('title', 'national_society', 'disaster_type', - 'ns_request_date', 'submission_to_geneva', 'status',) + search_fields = ("title",) + list_display = ( + "title", + "national_society", + "disaster_type", + "ns_request_date", + "submission_to_geneva", + "status", + ) autocomplete_fields = ( - 'national_society', - 'disaster_type', - 'users', - 'event_map', - 'images', - 'budget_file', - 'cover_image', + "national_society", + "disaster_type", + "users", + "event_map", + "images", + "budget_file", + "cover_image", ) def get_queryset(self, request): - return super().get_queryset(request).prefetch_related( - 'planned_interventions', - 'needs_identified', - 'national_society_actions', - 'users' + return ( + super() + .get_queryset(request) + .prefetch_related("planned_interventions", "needs_identified", "national_society_actions", "users") ) @admin.register(DrefOperationalUpdate) class DrefOperationalUpdateAdmin(CompareVersionAdmin, admin.ModelAdmin): - list_display = ('title', 'national_society', 'disaster_type') + list_display = ("title", "national_society", "disaster_type") autocomplete_fields = ( - 'national_society', - 'disaster_type', - 'images', - 'users', - 'event_map', - 'images', - 'budget_file', - 'cover_image', + "national_society", + "disaster_type", + "images", + "users", + "event_map", + "images", + "budget_file", + "cover_image", ) - list_filter = ['dref'] + list_filter = ["dref"] def get_queryset(self, request): - return super().get_queryset(request).prefetch_related( - 'planned_interventions', - 'needs_identified', - 'national_society_actions', - 'users' + return ( + super() + .get_queryset(request) + .prefetch_related("planned_interventions", "needs_identified", "national_society_actions", "users") ) + @admin.register(DrefFinalReport) class DrefFinalReportAdmin(CompareVersionAdmin, admin.ModelAdmin): - list_display = ('title', 'national_society', 'disaster_type') + list_display = ("title", "national_society", "disaster_type") autocomplete_fields = ( - 'national_society', - 'disaster_type', - 'photos', + "national_society", + "disaster_type", + "photos", ) - list_filter = ['dref'] - search_fields = ['title', 'national_society__name'] + list_filter = ["dref"] + search_fields = ["title", "national_society__name"] diff --git a/dref/apps.py b/dref/apps.py index b6dae0b52..c83cda251 100644 --- a/dref/apps.py +++ b/dref/apps.py @@ -3,5 +3,5 @@ class DrefConfig(AppConfig): - name = 'dref' - verbose_name = _('dref') + name = "dref" + verbose_name = _("dref") diff --git a/dref/factories/dref.py b/dref/factories/dref.py index edb531457..1bac4ec19 100644 --- a/dref/factories/dref.py +++ b/dref/factories/dref.py @@ -12,7 +12,7 @@ DrefFile, DrefOperationalUpdate, DrefFinalReport, - PlannedInterventionIndicators + PlannedInterventionIndicators, ) @@ -20,7 +20,7 @@ class DrefFactory(factory.django.DjangoModelFactory): class Meta: model = Dref - title = fuzzy.FuzzyText(length=50, prefix='title-') + title = fuzzy.FuzzyText(length=50, prefix="title-") type_of_onset = fuzzy.FuzzyChoice(Dref.OnsetType) disaster_category = fuzzy.FuzzyChoice(Dref.DisasterCategory) status = fuzzy.FuzzyChoice(Dref.Status) @@ -75,18 +75,15 @@ class Meta: model = DrefFile file = factory.LazyAttribute( - lambda _: ContentFile( - factory.django.ImageField()._make_data( - {'width': 1024, 'height': 768} - ), 'dref.jpg' - ) + lambda _: ContentFile(factory.django.ImageField()._make_data({"width": 1024, "height": 768}), "dref.jpg") ) class PlannedInterventionIndicatorsFactory(factory.django.DjangoModelFactory): class Meta: model = PlannedInterventionIndicators - title = fuzzy.FuzzyText(length=50, prefix='title-') + + title = fuzzy.FuzzyText(length=50, prefix="title-") class PlannedInterventionFactory(factory.django.DjangoModelFactory): @@ -122,7 +119,8 @@ class Meta: class DrefOperationalUpdateFactory(factory.django.DjangoModelFactory): class Meta: model = DrefOperationalUpdate - title = fuzzy.FuzzyText(length=50, prefix='title-') + + title = fuzzy.FuzzyText(length=50, prefix="title-") type_of_onset = fuzzy.FuzzyChoice(Dref.OnsetType) disaster_category = fuzzy.FuzzyChoice(Dref.DisasterCategory) national_society = factory.SubFactory(CountryFactory) @@ -159,7 +157,7 @@ class DrefFinalReportFactory(factory.django.DjangoModelFactory): class Meta: model = DrefFinalReport - title = fuzzy.FuzzyText(length=50, prefix='Final-Report-') + title = fuzzy.FuzzyText(length=50, prefix="Final-Report-") type_of_onset = fuzzy.FuzzyChoice(Dref.OnsetType) disaster_category = fuzzy.FuzzyChoice(Dref.DisasterCategory) national_society = factory.SubFactory(CountryFactory) diff --git a/dref/filter_set.py b/dref/filter_set.py index f46a5d919..ce534c102 100644 --- a/dref/filter_set.py +++ b/dref/filter_set.py @@ -1,34 +1,25 @@ import django_filters as filters -from dref.models import ( - Dref, - DrefOperationalUpdate -) +from dref.models import Dref, DrefOperationalUpdate from api.models import Country class DrefFilter(filters.FilterSet): status = filters.MultipleChoiceFilter( choices=Dref.Status.choices, - lookup_expr='in', + lookup_expr="in", widget=filters.widgets.CSVWidget, ) - country = filters.ModelMultipleChoiceFilter( - field_name='drefcountrydistrict__country', - queryset=Country.objects.all() - ) + country = filters.ModelMultipleChoiceFilter(field_name="country", queryset=Country.objects.all()) class Meta: model = Dref - fields = ['is_published'] + fields = ["is_published"] class DrefOperationalUpdateFilter(filters.FilterSet): - dref = filters.ModelMultipleChoiceFilter( - field_name='dref', - queryset=Dref.objects.all().distinct() - ) + dref = filters.ModelMultipleChoiceFilter(field_name="dref", queryset=Dref.objects.all().distinct()) class Meta: model = DrefOperationalUpdate - fields = ['is_published'] + fields = ["is_published"] diff --git a/dref/migrations/0046_auto_20221202_1030.py b/dref/migrations/0046_auto_20221202_1030.py new file mode 100644 index 000000000..f2c6ae682 --- /dev/null +++ b/dref/migrations/0046_auto_20221202_1030.py @@ -0,0 +1,56 @@ +# Generated by Django 3.2.16 on 2022-12-02 10:30 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('dref', '0045_alter_dref_modified_at'), + ] + + operations = [ + migrations.AddField( + model_name='dreffinalreport', + name='cover_image', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cover_image_dref_final_report', to='dref.dreffile', verbose_name='cover image'), + ), + migrations.AddField( + model_name='dreffinalreport', + name='images', + field=models.ManyToManyField(blank=True, related_name='image_dref_final_report', to='dref.DrefFile', verbose_name='images'), + ), + migrations.AddField( + model_name='dreffinalreport', + name='is_assessment_report', + field=models.BooleanField(blank=True, null=True, verbose_name='Is assessment Report'), + ), + migrations.AddField( + model_name='dreffinalreport', + name='is_there_major_coordination_mechanism', + field=models.BooleanField(blank=True, help_text='Is there major coordinate mechanism', null=True), + ), + migrations.AddField( + model_name='dreffinalreport', + name='risk_security', + field=models.ManyToManyField(blank=True, to='dref.RiskSecurity', verbose_name='Risk Security'), + ), + migrations.AddField( + model_name='dreffinalreport', + name='risk_security_concern', + field=models.TextField(blank=True, null=True, verbose_name='Risk Security Concern'), + ), + migrations.AddField( + model_name='dreffinalreport', + name='total_targeted_population', + field=models.IntegerField(blank=True, null=True, verbose_name='total targeted population'), + ), + migrations.AddField( + model_name='dreffinalreport', + name='users', + field=models.ManyToManyField(blank=True, related_name='user_dref_final_report', to=settings.AUTH_USER_MODEL, verbose_name='users'), + ), + ] diff --git a/dref/migrations/0047_auto_20221212_0750.py b/dref/migrations/0047_auto_20221212_0750.py new file mode 100644 index 000000000..1a7d5c5c6 --- /dev/null +++ b/dref/migrations/0047_auto_20221212_0750.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.16 on 2022-12-12 07:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dref', '0046_auto_20221202_1030'), + ] + + operations = [ + migrations.AddField( + model_name='dreffinalreport', + name='event_date', + field=models.DateField(blank=True, help_text='Date of event/Approximate date of impact', null=True, verbose_name='event date'), + ), + migrations.AddField( + model_name='dreffinalreport', + name='national_society_actions', + field=models.ManyToManyField(blank=True, to='dref.NationalSocietyAction', verbose_name='national society actions'), + ), + migrations.AddField( + model_name='dreffinalreport', + name='people_in_need', + field=models.IntegerField(blank=True, null=True, verbose_name='people in need'), + ), + ] diff --git a/dref/migrations/0048_auto_20221213_0402.py b/dref/migrations/0048_auto_20221213_0402.py new file mode 100644 index 000000000..0e7d47c54 --- /dev/null +++ b/dref/migrations/0048_auto_20221213_0402.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.16 on 2022-12-13 04:02 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dref', '0047_auto_20221212_0750'), + ] + + operations = [ + migrations.AddField( + model_name='dreffinalreport', + name='budget_file', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='budget_file_dref_final_report', to='dref.dreffile', verbose_name='budget file'), + ), + migrations.AddField( + model_name='dreffinalreport', + name='did_national_society', + field=models.BooleanField(blank=True, null=True, verbose_name='Did National Society'), + ), + migrations.AddField( + model_name='dreffinalreport', + name='event_text', + field=models.TextField(blank=True, null=True, verbose_name='event text'), + ), + migrations.AddField( + model_name='dreffinalreport', + name='ns_respond_date', + field=models.DateField(blank=True, help_text='NS anticipatory actions started/NS response', null=True, verbose_name='ns respond date'), + ), + ] diff --git a/dref/migrations/0049_auto_20221227_0501.py b/dref/migrations/0049_auto_20221227_0501.py new file mode 100644 index 000000000..5a583a5a4 --- /dev/null +++ b/dref/migrations/0049_auto_20221227_0501.py @@ -0,0 +1,48 @@ +# Generated by Django 3.2.16 on 2022-12-27 05:01 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dref', '0048_auto_20221213_0402'), + ] + + operations = [ + migrations.RemoveField( + model_name='dreffinalreport', + name='budget_file', + ), + migrations.AddField( + model_name='dreffinalreport', + name='financial_report', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='financial_report_dref_final_report', to='dref.dreffile', verbose_name='financial report'), + ), + migrations.AddField( + model_name='dreffinalreport', + name='financial_report_description', + field=models.TextField(blank=True, null=True, verbose_name='Financial Report Description'), + ), + migrations.AddField( + model_name='dreffinalreport', + name='has_national_society_conducted', + field=models.BooleanField(blank=True, null=True, verbose_name='Has national society conducted any intervention'), + ), + migrations.AddField( + model_name='dreffinalreport', + name='national_society_conducted_description', + field=models.TextField(blank=True, null=True, verbose_name='National Society conducted description'), + ), + migrations.AddField( + model_name='dreffinalreport', + name='num_assisted', + field=models.IntegerField(blank=True, null=True, verbose_name='number of assisted'), + ), + migrations.AddField( + model_name='plannedintervention', + name='person_assisted', + field=models.IntegerField(blank=True, null=True, verbose_name='person assisted'), + ), + ] diff --git a/dref/migrations/0050_dref_type_of_dref.py b/dref/migrations/0050_dref_type_of_dref.py new file mode 100644 index 000000000..81117d6fb --- /dev/null +++ b/dref/migrations/0050_dref_type_of_dref.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.17 on 2023-02-15 11:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dref', '0049_auto_20221227_0501'), + ] + + operations = [ + migrations.AddField( + model_name='dref', + name='type_of_dref', + field=models.IntegerField(blank=True, choices=[(0, 'Imminent'), (1, 'Assessment'), (2, 'Response')], null=True, verbose_name='dref type'), + ), + migrations.RunSQL( + sql=[("update dref_dref set type_of_dref = 2"), + ("update dref_dref set type_of_dref = 0 where type_of_onset=0"), + ("update dref_dref set type_of_dref = 1 where is_assessment_report"),], + reverse_sql=[("update dref_dref set type_of_dref = NULL")], + ) + ] diff --git a/dref/migrations/0051_drefoperationalupdate_type_of_dref.py b/dref/migrations/0051_drefoperationalupdate_type_of_dref.py new file mode 100644 index 000000000..334deb9ad --- /dev/null +++ b/dref/migrations/0051_drefoperationalupdate_type_of_dref.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.17 on 2023-02-15 12:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dref', '0050_dref_type_of_dref'), + ] + + operations = [ + migrations.AddField( + model_name='drefoperationalupdate', + name='type_of_dref', + field=models.IntegerField(blank=True, choices=[(0, 'Imminent'), (1, 'Assessment'), (2, 'Response')], null=True, verbose_name='dref type'), + ), + migrations.RunSQL( + sql=[("update dref_drefoperationalupdate set type_of_dref = 2"), + ("update dref_drefoperationalupdate set type_of_dref = 0 where type_of_onset=0"), + ("update dref_drefoperationalupdate set type_of_dref = 1 where is_assessment_report"),], + reverse_sql=[("update dref_drefoperationalupdate set type_of_dref = NULL")], + ) + ] diff --git a/dref/migrations/0052_dreffinalreport_type_of_dref.py b/dref/migrations/0052_dreffinalreport_type_of_dref.py new file mode 100644 index 000000000..93a39695d --- /dev/null +++ b/dref/migrations/0052_dreffinalreport_type_of_dref.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.17 on 2023-02-15 12:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dref', '0051_drefoperationalupdate_type_of_dref'), + ] + + operations = [ + migrations.AddField( + model_name='dreffinalreport', + name='type_of_dref', + field=models.IntegerField(blank=True, choices=[(0, 'Imminent'), (1, 'Assessment'), (2, 'Response')], null=True, verbose_name='dref type'), + ), + migrations.RunSQL( + sql=[("update dref_dreffinalreport set type_of_dref = 2"), + ("update dref_dreffinalreport set type_of_dref = 0 where type_of_onset=0"), + ("update dref_dreffinalreport set type_of_dref = 1 where is_assessment_report"),], + reverse_sql=[("update dref_dreffinalreport set type_of_dref = NULL")], + ) + ] diff --git a/dref/models.py b/dref/models.py index 1ecad3ebb..57f897728 100644 --- a/dref/models.py +++ b/dref/models.py @@ -1,7 +1,6 @@ import reversion import os import copy -from datetime import datetime from pdf2image import convert_from_bytes @@ -10,125 +9,125 @@ from django.utils.translation import gettext_lazy as _ from django.templatetags.static import static from django.core.exceptions import ValidationError +from django.contrib.postgres.aggregates import ArrayAgg -from api.models import ( - Country, - DisasterType, - District, - FieldReport -) +from api.models import Country, DisasterType, District, FieldReport @reversion.register() class NationalSocietyAction(models.Model): # NOTE: Replace `TextChoices` to `models.TextChoices` after upgrade to Django version 3 class Title(models.TextChoices): - NATIONAL_SOCIETY_READINESS = 'national_society_readiness', _('National Society Readiness') - ASSESSMENT = 'assessment', _('Assessment') - COORDINATION = 'coordination', _('Coordination') - RESOURCE_MOBILIZATION = 'resource_mobilization', _('Resource Mobilization') - ACTIVATION_OF_CONTINGENCY_PLANS = 'activation_of_contingency_plans', _('Activation Of Contingency Plans') - NATIONAL_SOCIETY_EOC = 'national_society_eoc', _('National Society EOC') - SHELTER_HOUSING_AND_SETTLEMENTS = 'shelter_housing_and_settlements', _('Shelter, Housing And Settlements') - LIVELIHOODS_AND_BASIC_NEEDS = 'livelihoods_and_basic_needs', _('Livelihoods And Basic Needs') - HEALTH = 'health', _('Health') - WATER_SANITATION_AND_HYGIENE = 'water_sanitation_and_hygiene', _('Water, Sanitation And Hygiene') - PROTECTION_GENDER_AND_INCLUSION = 'protection_gender_and_inclusion', _('Protection, Gender And Inclusion') - EDUCATION = 'education', _('Education') - MIGRATION = 'migration', _('Migration') - RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY = \ - 'risk_reduction_climate_adaptation_and_recovery', _('Risk Reduction, Climate Adaptation And Recovery') - COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY = \ - 'community_engagement_and _accountability', _('Community Engagement And Accountability') - ENVIRONMENT_SUSTAINABILITY = 'environment_sustainability ', _('Environment Sustainability') - MULTI_PURPOSE_CASH = 'multi-purpose_cash', _('Multi-purpose Cash') - OTHER = 'other', _('Other') + NATIONAL_SOCIETY_READINESS = "national_society_readiness", _("National Society Readiness") + ASSESSMENT = "assessment", _("Assessment") + COORDINATION = "coordination", _("Coordination") + RESOURCE_MOBILIZATION = "resource_mobilization", _("Resource Mobilization") + ACTIVATION_OF_CONTINGENCY_PLANS = "activation_of_contingency_plans", _("Activation Of Contingency Plans") + NATIONAL_SOCIETY_EOC = "national_society_eoc", _("National Society EOC") + SHELTER_HOUSING_AND_SETTLEMENTS = "shelter_housing_and_settlements", _("Shelter, Housing And Settlements") + LIVELIHOODS_AND_BASIC_NEEDS = "livelihoods_and_basic_needs", _("Livelihoods And Basic Needs") + HEALTH = "health", _("Health") + WATER_SANITATION_AND_HYGIENE = "water_sanitation_and_hygiene", _("Water, Sanitation And Hygiene") + PROTECTION_GENDER_AND_INCLUSION = "protection_gender_and_inclusion", _("Protection, Gender And Inclusion") + EDUCATION = "education", _("Education") + MIGRATION = "migration", _("Migration") + RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY = "risk_reduction_climate_adaptation_and_recovery", _( + "Risk Reduction, Climate Adaptation And Recovery" + ) + COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY = "community_engagement_and _accountability", _( + "Community Engagement And Accountability" + ) + ENVIRONMENT_SUSTAINABILITY = "environment_sustainability ", _("Environment Sustainability") + MULTI_PURPOSE_CASH = "multi-purpose_cash", _("Multi-purpose Cash") + OTHER = "other", _("Other") - title = models.CharField(max_length=255, verbose_name=_('title'), choices=Title.choices) - description = models.TextField(verbose_name=_('description'), blank=True, null=True) + title = models.CharField(max_length=255, verbose_name=_("title"), choices=Title.choices) + description = models.TextField(verbose_name=_("description"), blank=True, null=True) class Meta: - verbose_name = _('national society action') - verbose_name_plural = _('national society actions') + verbose_name = _("national society action") + verbose_name_plural = _("national society actions") @staticmethod def get_image_map(title, request): title_static_map = { - NationalSocietyAction.Title.SHELTER_HOUSING_AND_SETTLEMENTS: 'shelter.png', - NationalSocietyAction.Title.LIVELIHOODS_AND_BASIC_NEEDS: 'livelihood.png', - NationalSocietyAction.Title.HEALTH: 'health.png', - NationalSocietyAction.Title.WATER_SANITATION_AND_HYGIENE: 'water.png', - NationalSocietyAction.Title.PROTECTION_GENDER_AND_INCLUSION: 'protection.png', - NationalSocietyAction.Title.EDUCATION: 'education.png', - NationalSocietyAction.Title.MIGRATION: 'migration.png', - NationalSocietyAction.Title.RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY: 'risk.png', - NationalSocietyAction.Title.ENVIRONMENT_SUSTAINABILITY: 'environment.png', - NationalSocietyAction.Title.NATIONAL_SOCIETY_READINESS: 'favicon.png', - NationalSocietyAction.Title.ASSESSMENT: 'favicon.png', - NationalSocietyAction.Title.COORDINATION: 'favicon.png', - NationalSocietyAction.Title.RESOURCE_MOBILIZATION: 'favicon.png', - NationalSocietyAction.Title.ACTIVATION_OF_CONTINGENCY_PLANS: 'favicon.png', - NationalSocietyAction.Title.NATIONAL_SOCIETY_EOC: 'favicon.png', - NationalSocietyAction.Title.COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY: 'favicon.png', - NationalSocietyAction.Title.MULTI_PURPOSE_CASH: 'cash.png', - NationalSocietyAction.Title.OTHER: 'favicon.png', + NationalSocietyAction.Title.SHELTER_HOUSING_AND_SETTLEMENTS: "shelter.png", + NationalSocietyAction.Title.LIVELIHOODS_AND_BASIC_NEEDS: "livelihood.png", + NationalSocietyAction.Title.HEALTH: "health.png", + NationalSocietyAction.Title.WATER_SANITATION_AND_HYGIENE: "water.png", + NationalSocietyAction.Title.PROTECTION_GENDER_AND_INCLUSION: "protection.png", + NationalSocietyAction.Title.EDUCATION: "education.png", + NationalSocietyAction.Title.MIGRATION: "migration.png", + NationalSocietyAction.Title.RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY: "risk.png", + NationalSocietyAction.Title.ENVIRONMENT_SUSTAINABILITY: "environment.png", + NationalSocietyAction.Title.NATIONAL_SOCIETY_READINESS: "favicon.png", + NationalSocietyAction.Title.ASSESSMENT: "favicon.png", + NationalSocietyAction.Title.COORDINATION: "favicon.png", + NationalSocietyAction.Title.RESOURCE_MOBILIZATION: "favicon.png", + NationalSocietyAction.Title.ACTIVATION_OF_CONTINGENCY_PLANS: "favicon.png", + NationalSocietyAction.Title.NATIONAL_SOCIETY_EOC: "favicon.png", + NationalSocietyAction.Title.COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY: "favicon.png", + NationalSocietyAction.Title.MULTI_PURPOSE_CASH: "cash.png", + NationalSocietyAction.Title.OTHER: "favicon.png", } - return request.build_absolute_uri(static(os.path.join('images/dref', title_static_map[title]))) + return request.build_absolute_uri(static(os.path.join("images/dref", title_static_map[title]))) @reversion.register() class IdentifiedNeed(models.Model): class Title(models.TextChoices): - SHELTER_HOUSING_AND_SETTLEMENTS = 'shelter_housing_and_settlements', _('Shelter Housing And Settlements') - LIVELIHOODS_AND_BASIC_NEEDS = 'livelihoods_and_basic_needs', _('Livelihoods And Basic Needs') - HEALTH = 'health', _('Health') - WATER_SANITATION_AND_HYGIENE = 'water_sanitation_and_hygiene', _('Water, Sanitation And Hygiene') - PROTECTION_GENDER_AND_INCLUSION = 'protection_gender_and_inclusion', _('Protection, Gender And Inclusion') - EDUCATION = 'education', _('Education') - MIGRATION = 'migration', _('Migration') - MULTI_PURPOSE_CASH_GRANTS = 'multi_purpose_cash_grants', _('Multi purpose cash grants') - RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY = \ - 'risk_reduction_climate_adaptation_and_recovery', _('Risk Reduction, Climate Adaptation And Recovery') - COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY = \ - 'community_engagement_and _accountability', _('Community Engagement And Accountability') - ENVIRONMENT_SUSTAINABILITY = 'environment_sustainability ', _('Environment Sustainability') - SHELTER_CLUSTER_COORDINATION = 'shelter_cluster_coordination', _('Shelter Cluster Coordination') + SHELTER_HOUSING_AND_SETTLEMENTS = "shelter_housing_and_settlements", _("Shelter Housing And Settlements") + LIVELIHOODS_AND_BASIC_NEEDS = "livelihoods_and_basic_needs", _("Livelihoods And Basic Needs") + HEALTH = "health", _("Health") + WATER_SANITATION_AND_HYGIENE = "water_sanitation_and_hygiene", _("Water, Sanitation And Hygiene") + PROTECTION_GENDER_AND_INCLUSION = "protection_gender_and_inclusion", _("Protection, Gender And Inclusion") + EDUCATION = "education", _("Education") + MIGRATION = "migration", _("Migration") + MULTI_PURPOSE_CASH_GRANTS = "multi_purpose_cash_grants", _("Multi purpose cash grants") + RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY = "risk_reduction_climate_adaptation_and_recovery", _( + "Risk Reduction, Climate Adaptation And Recovery" + ) + COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY = "community_engagement_and _accountability", _( + "Community Engagement And Accountability" + ) + ENVIRONMENT_SUSTAINABILITY = "environment_sustainability ", _("Environment Sustainability") + SHELTER_CLUSTER_COORDINATION = "shelter_cluster_coordination", _("Shelter Cluster Coordination") - title = models.CharField(max_length=255, verbose_name=_('title'), choices=Title.choices) - description = models.TextField(verbose_name=_('description'), blank=True, null=True) + title = models.CharField(max_length=255, verbose_name=_("title"), choices=Title.choices) + description = models.TextField(verbose_name=_("description"), blank=True, null=True) class Meta: - verbose_name = _('identified need') - verbose_name_plural = _('identified needs') + verbose_name = _("identified need") + verbose_name_plural = _("identified needs") @staticmethod def get_image_map(title, request): title_static_map = { - IdentifiedNeed.Title.SHELTER_HOUSING_AND_SETTLEMENTS: 'shelter.png', - IdentifiedNeed.Title.LIVELIHOODS_AND_BASIC_NEEDS: 'livelihood.png', - IdentifiedNeed.Title.HEALTH: 'health.png', - IdentifiedNeed.Title.WATER_SANITATION_AND_HYGIENE: 'water.png', - IdentifiedNeed.Title.PROTECTION_GENDER_AND_INCLUSION: 'protection.png', - IdentifiedNeed.Title.MULTI_PURPOSE_CASH_GRANTS: 'cash.png', - IdentifiedNeed.Title.EDUCATION: 'education.png', - IdentifiedNeed.Title.MIGRATION: 'migration.png', - IdentifiedNeed.Title.RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY: 'risk.png', - IdentifiedNeed.Title.ENVIRONMENT_SUSTAINABILITY: 'environment.png', - IdentifiedNeed.Title.COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY: 'participation_team.png', - IdentifiedNeed.Title.SHELTER_CLUSTER_COORDINATION: 'migration.png', + IdentifiedNeed.Title.SHELTER_HOUSING_AND_SETTLEMENTS: "shelter.png", + IdentifiedNeed.Title.LIVELIHOODS_AND_BASIC_NEEDS: "livelihood.png", + IdentifiedNeed.Title.HEALTH: "health.png", + IdentifiedNeed.Title.WATER_SANITATION_AND_HYGIENE: "water.png", + IdentifiedNeed.Title.PROTECTION_GENDER_AND_INCLUSION: "protection.png", + IdentifiedNeed.Title.MULTI_PURPOSE_CASH_GRANTS: "cash.png", + IdentifiedNeed.Title.EDUCATION: "education.png", + IdentifiedNeed.Title.MIGRATION: "migration.png", + IdentifiedNeed.Title.RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY: "risk.png", + IdentifiedNeed.Title.ENVIRONMENT_SUSTAINABILITY: "environment.png", + IdentifiedNeed.Title.COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY: "participation_team.png", + IdentifiedNeed.Title.SHELTER_CLUSTER_COORDINATION: "migration.png", } - return request.build_absolute_uri(static(os.path.join('images/dref', title_static_map[title]))) + return request.build_absolute_uri(static(os.path.join("images/dref", title_static_map[title]))) @reversion.register() class PlannedInterventionIndicators(models.Model): - title = models.CharField(max_length=255, verbose_name='Title') - target = models.IntegerField(verbose_name=_('Target'), null=True, blank=True) - actual = models.IntegerField(verbose_name=_('Actual'), null=True, blank=True) + title = models.CharField(max_length=255, verbose_name="Title") + target = models.IntegerField(verbose_name=_("Target"), null=True, blank=True) + actual = models.IntegerField(verbose_name=_("Actual"), null=True, blank=True) class Meta: - verbose_name = _('planned intervention indicator') - verbose_name_plural = _('planned intervention indicators') + verbose_name = _("planned intervention indicator") + verbose_name_plural = _("planned intervention indicators") def __str__(self): return self.title @@ -136,510 +135,439 @@ def __str__(self): class PlannedIntervention(models.Model): class Title(models.TextChoices): - SHELTER_HOUSING_AND_SETTLEMENTS = 'shelter_housing_and_settlements', _('Shelter Housing And Settlements') - LIVELIHOODS_AND_BASIC_NEEDS = 'livelihoods_and_basic_needs', _('Livelihoods And Basic Needs') - HEALTH = 'health', _('Health') - WATER_SANITATION_AND_HYGIENE = 'water_sanitation_and_hygiene', _('Water, Sanitation And Hygiene') - PROTECTION_GENDER_AND_INCLUSION = 'protection_gender_and_inclusion', _('Protection, Gender And Inclusion') - EDUCATION = 'education', _('Education') - MIGRATION = 'migration', _('Migration') - RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY = \ - 'risk_reduction_climate_adaptation_and_recovery_', _('Risk Reduction, Climate Adaptation And Recovery') - SECRETARIAT_SERVICES = 'secretariat_services', _('Secretariat Services') - NATIONAL_SOCIETY_STRENGTHENING = 'national_society_strengthening', _('National Society Strengthening') - MULTI_PURPOSE_CASH = 'multi-purpose_cash', _('Multi-purpose Cash') - ENVIRONMENTAL_SUSTAINABILITY = 'environmental_sustainability', _('Environmental Sustainability') - COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY = 'community_engagement_and_accountability', _('Community Engagement And Accountability') + SHELTER_HOUSING_AND_SETTLEMENTS = "shelter_housing_and_settlements", _("Shelter Housing And Settlements") + LIVELIHOODS_AND_BASIC_NEEDS = "livelihoods_and_basic_needs", _("Livelihoods And Basic Needs") + HEALTH = "health", _("Health") + WATER_SANITATION_AND_HYGIENE = "water_sanitation_and_hygiene", _("Water, Sanitation And Hygiene") + PROTECTION_GENDER_AND_INCLUSION = "protection_gender_and_inclusion", _("Protection, Gender And Inclusion") + EDUCATION = "education", _("Education") + MIGRATION = "migration", _("Migration") + RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY = "risk_reduction_climate_adaptation_and_recovery_", _( + "Risk Reduction, Climate Adaptation And Recovery" + ) + SECRETARIAT_SERVICES = "secretariat_services", _("Secretariat Services") + NATIONAL_SOCIETY_STRENGTHENING = "national_society_strengthening", _("National Society Strengthening") + MULTI_PURPOSE_CASH = "multi-purpose_cash", _("Multi-purpose Cash") + ENVIRONMENTAL_SUSTAINABILITY = "environmental_sustainability", _("Environmental Sustainability") + COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY = "community_engagement_and_accountability", _( + "Community Engagement And Accountability" + ) - title = models.CharField(max_length=255, verbose_name=_('title'), choices=Title.choices) - description = models.TextField(verbose_name=_('description'), blank=True, null=True) - person_targeted = models.IntegerField(verbose_name=_('person targeted'), null=True, blank=True) - budget = models.IntegerField(verbose_name=_('budget'), blank=True, null=True) - male = models.IntegerField(verbose_name=_('male'), blank=True, null=True) - female = models.IntegerField(verbose_name=_('female'), blank=True, null=True) + title = models.CharField(max_length=255, verbose_name=_("title"), choices=Title.choices) + description = models.TextField(verbose_name=_("description"), blank=True, null=True) + person_targeted = models.IntegerField(verbose_name=_("person targeted"), null=True, blank=True) + person_assisted = models.IntegerField(verbose_name=_("person assisted"), null=True, blank=True) + budget = models.IntegerField(verbose_name=_("budget"), blank=True, null=True) + male = models.IntegerField(verbose_name=_("male"), blank=True, null=True) + female = models.IntegerField(verbose_name=_("female"), blank=True, null=True) indicators = models.ManyToManyField( PlannedInterventionIndicators, - verbose_name=_('Indicators'), + verbose_name=_("Indicators"), blank=True, ) - progress_towards_outcome = models.TextField( - verbose_name=_('Progress Towards Outcome'), - blank=True, null=True - ) + progress_towards_outcome = models.TextField(verbose_name=_("Progress Towards Outcome"), blank=True, null=True) narrative_description_of_achievements = models.TextField( - verbose_name=_('Narrative description of achievements'), - blank=True, null=True - ) - challenges = models.TextField( - verbose_name=_('Challenges'), - null=True, blank=True - ) - lessons_learnt = models.TextField( - verbose_name=_('Lessons learnt'), - null=True, blank=True + verbose_name=_("Narrative description of achievements"), blank=True, null=True ) + challenges = models.TextField(verbose_name=_("Challenges"), null=True, blank=True) + lessons_learnt = models.TextField(verbose_name=_("Lessons learnt"), null=True, blank=True) class Meta: - verbose_name = _('planned intervention') - verbose_name_plural = _('planned interventions') + verbose_name = _("planned intervention") + verbose_name_plural = _("planned interventions") @staticmethod def get_image_map(title, request): title_static_map = { - PlannedIntervention.Title.SHELTER_HOUSING_AND_SETTLEMENTS: 'shelter.png', - PlannedIntervention.Title.LIVELIHOODS_AND_BASIC_NEEDS: 'livelihood.png', - PlannedIntervention.Title.HEALTH: 'health.png', - PlannedIntervention.Title.WATER_SANITATION_AND_HYGIENE: 'water.png', - PlannedIntervention.Title.PROTECTION_GENDER_AND_INCLUSION: 'protection.png', - PlannedIntervention.Title.EDUCATION: 'education.png', - PlannedIntervention.Title.MIGRATION: 'migration.png', - PlannedIntervention.Title.RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY: 'risk.png', - PlannedIntervention.Title.SECRETARIAT_SERVICES: 'work.png', - PlannedIntervention.Title.NATIONAL_SOCIETY_STRENGTHENING: 'independence.png', - PlannedIntervention.Title.MULTI_PURPOSE_CASH: 'cash.png', - PlannedIntervention.Title.ENVIRONMENTAL_SUSTAINABILITY: 'environment.png', - PlannedIntervention.Title.COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY: 'participation_team.png', + PlannedIntervention.Title.SHELTER_HOUSING_AND_SETTLEMENTS: "shelter.png", + PlannedIntervention.Title.LIVELIHOODS_AND_BASIC_NEEDS: "livelihood.png", + PlannedIntervention.Title.HEALTH: "health.png", + PlannedIntervention.Title.WATER_SANITATION_AND_HYGIENE: "water.png", + PlannedIntervention.Title.PROTECTION_GENDER_AND_INCLUSION: "protection.png", + PlannedIntervention.Title.EDUCATION: "education.png", + PlannedIntervention.Title.MIGRATION: "migration.png", + PlannedIntervention.Title.RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY: "risk.png", + PlannedIntervention.Title.SECRETARIAT_SERVICES: "work.png", + PlannedIntervention.Title.NATIONAL_SOCIETY_STRENGTHENING: "independence.png", + PlannedIntervention.Title.MULTI_PURPOSE_CASH: "cash.png", + PlannedIntervention.Title.ENVIRONMENTAL_SUSTAINABILITY: "environment.png", + PlannedIntervention.Title.COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY: "participation_team.png", } - return request.build_absolute_uri(static(os.path.join('images/dref', title_static_map[title]))) + return request.build_absolute_uri(static(os.path.join("images/dref", title_static_map[title]))) @reversion.register() class RiskSecurity(models.Model): - client_id = models.CharField(max_length=50, null=True, blank=True, verbose_name=_('client_id')) - risk = models.TextField(verbose_name=_('Risk'), null=True, blank=True) - mitigation = models.TextField(verbose_name=_('Mitigation'), null=True, blank=True) + client_id = models.CharField(max_length=50, null=True, blank=True, verbose_name=_("client_id")) + risk = models.TextField(verbose_name=_("Risk"), null=True, blank=True) + mitigation = models.TextField(verbose_name=_("Mitigation"), null=True, blank=True) @reversion.register() class Dref(models.Model): + class DrefType(models.IntegerChoices): + IMMINENT = 0, _("Imminent") + ASSESSMENT = 1, _("Assessment") + RESPONSE = 2, _("Response") class OnsetType(models.IntegerChoices): - IMMINENT = 0, _('Imminent') - SLOW = 1, _('Slow') - SUDDEN = 2, _('Sudden') + IMMINENT = 0, _("Imminent") + SLOW = 1, _("Slow") + SUDDEN = 2, _("Sudden") class DisasterCategory(models.IntegerChoices): - YELLOW = 0, _('Yellow') - ORANGE = 1, _('Orange') - RED = 2, _('Red') + YELLOW = 0, _("Yellow") + ORANGE = 1, _("Orange") + RED = 2, _("Red") class Status(models.IntegerChoices): - IN_PROGRESS = 0, _('In Progress') - COMPLETED = 1, _('Completed') + IN_PROGRESS = 0, _("In Progress") + COMPLETED = 1, _("Completed") - created_at = models.DateTimeField(verbose_name=_('created at'), auto_now_add=True) - modified_at = models.DateTimeField( - verbose_name=_('modified at'), - auto_now=True, - blank=True - ) + created_at = models.DateTimeField(verbose_name=_("created at"), auto_now_add=True) + modified_at = models.DateTimeField(verbose_name=_("modified at"), auto_now=True, blank=True) created_by = models.ForeignKey( - settings.AUTH_USER_MODEL, verbose_name=_('created by'), on_delete=models.SET_NULL, - null=True, related_name='created_by_dref' + settings.AUTH_USER_MODEL, + verbose_name=_("created by"), + on_delete=models.SET_NULL, + null=True, + related_name="created_by_dref", ) modified_by = models.ForeignKey( - settings.AUTH_USER_MODEL, verbose_name=_('modified by'), on_delete=models.SET_NULL, - null=True, related_name='modified_by_dref' - ) - users = models.ManyToManyField( - settings.AUTH_USER_MODEL, verbose_name=_('users'), - blank=True, related_name='user_dref' + settings.AUTH_USER_MODEL, + verbose_name=_("modified by"), + on_delete=models.SET_NULL, + null=True, + related_name="modified_by_dref", ) + users = models.ManyToManyField(settings.AUTH_USER_MODEL, verbose_name=_("users"), blank=True, related_name="user_dref") field_report = models.ForeignKey( - FieldReport, verbose_name=_('field report'), - on_delete=models.SET_NULL, null=True, blank=True, - related_name='field_report_dref' + FieldReport, + verbose_name=_("field report"), + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="field_report_dref", ) - title = models.CharField(verbose_name=_('title'), max_length=255) - title_prefix = models.CharField(verbose_name=_('title prefix'), max_length=255, null=True, blank=True) + title = models.CharField(verbose_name=_("title"), max_length=255) + title_prefix = models.CharField(verbose_name=_("title prefix"), max_length=255, null=True, blank=True) national_society = models.ForeignKey( - Country, verbose_name=_('national_society'), + Country, + verbose_name=_("national_society"), on_delete=models.CASCADE, ) disaster_type = models.ForeignKey( - DisasterType, verbose_name=_('disaster type'), - blank=True, null=True, - on_delete=models.SET_NULL - ) - type_of_onset = models.IntegerField(choices=OnsetType.choices, verbose_name=_('onset type'), null=True, blank=True) - disaster_category = models.IntegerField(choices=DisasterCategory.choices, verbose_name=_('disaster category'), null=True, blank=True) - status = models.IntegerField(choices=Status.choices, verbose_name=_('status'), null=True, blank=True) - num_assisted = models.IntegerField(verbose_name=_('number of assisted'), blank=True, null=True) - num_affected = models.IntegerField(verbose_name=_('number of affected'), blank=True, null=True) - amount_requested = models.IntegerField(verbose_name=_('amount requested'), blank=True, null=True) - people_in_need = models.IntegerField(verbose_name=_('people in need'), blank=True, null=True) - emergency_appeal_planned = models.BooleanField( - verbose_name=_('emergency appeal planned '), - null=True, blank=True + DisasterType, verbose_name=_("disaster type"), blank=True, null=True, on_delete=models.SET_NULL ) + type_of_dref = models.IntegerField(choices=DrefType.choices, verbose_name=_("dref type"), null=True, blank=True) + type_of_onset = models.IntegerField(choices=OnsetType.choices, verbose_name=_("onset type"), null=True, blank=True) + disaster_category = models.IntegerField( + choices=DisasterCategory.choices, verbose_name=_("disaster category"), null=True, blank=True + ) + status = models.IntegerField(choices=Status.choices, verbose_name=_("status"), null=True, blank=True) + num_assisted = models.IntegerField(verbose_name=_("number of assisted"), blank=True, null=True) + num_affected = models.IntegerField(verbose_name=_("number of affected"), blank=True, null=True) + amount_requested = models.IntegerField(verbose_name=_("amount requested"), blank=True, null=True) + people_in_need = models.IntegerField(verbose_name=_("people in need"), blank=True, null=True) + emergency_appeal_planned = models.BooleanField(verbose_name=_("emergency appeal planned "), null=True, blank=True) event_date = models.DateField( - verbose_name=_('event date'), - null=True, blank=True, - help_text=_('Date of event/Approximate date of impact') + verbose_name=_("event date"), null=True, blank=True, help_text=_("Date of event/Approximate date of impact") ) - event_text = models.TextField(verbose_name=_('event text'), blank=True, null=True) + event_text = models.TextField(verbose_name=_("event text"), blank=True, null=True) ns_respond_date = models.DateField( - verbose_name=_('ns respond date'), - null=True, blank=True, - help_text=_('NS anticipatory actions started/NS response') + verbose_name=_("ns respond date"), null=True, blank=True, help_text=_("NS anticipatory actions started/NS response") ) affect_same_area = models.BooleanField( - null=True, blank=True, - help_text=_('Has a similar event affected the same areas in the past?') - ) - affect_same_population = models.BooleanField( - null=True, blank=True, - help_text=_('Did it affect the same population?') - ) - affect_same_population_text = models.TextField( - blank=True, null=True, - verbose_name=_('affect same population text') - ) - ns_respond = models.BooleanField( - null=True, blank=True, - default=False, help_text=_('Did NS respond') + null=True, blank=True, help_text=_("Has a similar event affected the same areas in the past?") ) + affect_same_population = models.BooleanField(null=True, blank=True, help_text=_("Did it affect the same population?")) + affect_same_population_text = models.TextField(blank=True, null=True, verbose_name=_("affect same population text")) + ns_respond = models.BooleanField(null=True, blank=True, default=False, help_text=_("Did NS respond")) ns_request_fund = models.BooleanField( - null=True, blank=True, - default=False, help_text=_('Did the NS request funding from DREF?') + null=True, blank=True, default=False, help_text=_("Did the NS request funding from DREF?") ) - ns_request_text = models.TextField( - blank=True, null=True, - verbose_name=_('ns request text') - ) - dref_recurrent_text = models.TextField(verbose_name=_('dref recurrent text'), blank=True, null=True) - lessons_learned = models.TextField(verbose_name=_('lessons learned'), blank=True, null=True) - event_description = models.TextField(verbose_name=_('event description'), blank=True, null=True) + ns_request_text = models.TextField(blank=True, null=True, verbose_name=_("ns request text")) + dref_recurrent_text = models.TextField(verbose_name=_("dref recurrent text"), blank=True, null=True) + lessons_learned = models.TextField(verbose_name=_("lessons learned"), blank=True, null=True) + event_description = models.TextField(verbose_name=_("event description"), blank=True, null=True) anticipatory_actions = models.TextField( - blank=True, null=True, - verbose_name=_('anticipatory actions'), - help_text=_('Description of anticipatory actions or imminent disaster') + blank=True, + null=True, + verbose_name=_("anticipatory actions"), + help_text=_("Description of anticipatory actions or imminent disaster"), ) event_scope = models.TextField( - blank=True, null=True, - verbose_name=_('event scope'), - help_text=_('Scope and scale of event') + blank=True, null=True, verbose_name=_("event scope"), help_text=_("Scope and scale of event") ) national_society_actions = models.ManyToManyField( - NationalSocietyAction, verbose_name=_('national society actions'), - blank=True + NationalSocietyAction, verbose_name=_("national society actions"), blank=True ) government_requested_assistance = models.BooleanField( - null=True, blank=True, - help_text=_('Has government requested assistance') + null=True, blank=True, help_text=_("Has government requested assistance") ) government_requested_assistance_date = models.DateField( - verbose_name=_('government requested assistance date'), - null=True, blank=True - ) - national_authorities = models.TextField(verbose_name=_('national authorities'), blank=True, null=True) - ifrc = models.TextField(verbose_name=_('ifrc'), blank=True, null=True) - icrc = models.TextField(verbose_name=_('icrc'), blank=True, null=True) - partner_national_society = models.TextField(verbose_name=_('partner national society'), blank=True, null=True) - un_or_other_actor = models.TextField(verbose_name=_('un or other'), blank=True, null=True) + verbose_name=_("government requested assistance date"), null=True, blank=True + ) + national_authorities = models.TextField(verbose_name=_("national authorities"), blank=True, null=True) + ifrc = models.TextField(verbose_name=_("ifrc"), blank=True, null=True) + icrc = models.TextField(verbose_name=_("icrc"), blank=True, null=True) + partner_national_society = models.TextField(verbose_name=_("partner national society"), blank=True, null=True) + un_or_other_actor = models.TextField(verbose_name=_("un or other"), blank=True, null=True) is_there_major_coordination_mechanism = models.BooleanField( - blank=True, null=True, - verbose_name=_('Is major coordination mechanism'), + blank=True, + null=True, + verbose_name=_("Is major coordination mechanism"), ) major_coordination_mechanism = models.TextField( - blank=True, null=True, - verbose_name=_('major coordination mechanism'), - help_text=_('List major coordination mechanisms in place') - ) - needs_identified = models.ManyToManyField( - IdentifiedNeed, verbose_name=_('needs identified'), - blank=True + blank=True, + null=True, + verbose_name=_("major coordination mechanism"), + help_text=_("List major coordination mechanisms in place"), ) + needs_identified = models.ManyToManyField(IdentifiedNeed, verbose_name=_("needs identified"), blank=True) identified_gaps = models.TextField( - verbose_name=_('identified gaps'), blank=True, null=True, - help_text=_('Any identified gaps/limitations in the assessment') + verbose_name=_("identified gaps"), + blank=True, + null=True, + help_text=_("Any identified gaps/limitations in the assessment"), ) - people_assisted = models.TextField(verbose_name=_('people assisted'), blank=True, null=True) + people_assisted = models.TextField(verbose_name=_("people assisted"), blank=True, null=True) selection_criteria = models.TextField( - verbose_name=_('selection criteria'), blank=True, null=True, - help_text=_('Selection criteria for affected people') + verbose_name=_("selection criteria"), blank=True, null=True, help_text=_("Selection criteria for affected people") ) entity_affected = models.TextField( - verbose_name=_('entity affected'), blank=True, null=True, - help_text=_('Protection, gender, Inclusion affected in this process') + verbose_name=_("entity affected"), + blank=True, + null=True, + help_text=_("Protection, gender, Inclusion affected in this process"), ) community_involved = models.TextField( - verbose_name=_('community involved'), blank=True, null=True, - help_text=_('Community been involved in the analysis of the process') - ) - women = models.IntegerField(verbose_name=_('women'), blank=True, null=True) - men = models.IntegerField(verbose_name=_('men'), blank=True, null=True) - girls = models.IntegerField( - verbose_name=_('girls'), help_text=_('Girls under 18'), - blank=True, null=True - ) - boys = models.IntegerField( - verbose_name=_('boys'), help_text=_('Boys under 18'), - blank=True, null=True + verbose_name=_("community involved"), + blank=True, + null=True, + help_text=_("Community been involved in the analysis of the process"), ) + women = models.IntegerField(verbose_name=_("women"), blank=True, null=True) + men = models.IntegerField(verbose_name=_("men"), blank=True, null=True) + girls = models.IntegerField(verbose_name=_("girls"), help_text=_("Girls under 18"), blank=True, null=True) + boys = models.IntegerField(verbose_name=_("boys"), help_text=_("Boys under 18"), blank=True, null=True) total_targeted_population = models.IntegerField( - verbose_name=_('total targeted population'), help_text=_('Estimated number of targeted people'), - blank=True, null=True + verbose_name=_("total targeted population"), + help_text=_("Estimated number of targeted people"), + blank=True, + null=True, ) disability_people_per = models.DecimalField( - verbose_name=_('disability people per'), help_text=_('Estimated % people disability'), - blank=True, null=True, - max_digits=5, decimal_places=2 + verbose_name=_("disability people per"), + help_text=_("Estimated % people disability"), + blank=True, + null=True, + max_digits=5, + decimal_places=2, ) people_per_urban = models.DecimalField( - verbose_name=_('people per urban'), help_text=_('Estimated % people Urban'), - blank=True, null=True, - max_digits=5, decimal_places=2 + verbose_name=_("people per urban"), + help_text=_("Estimated % people Urban"), + blank=True, + null=True, + max_digits=5, + decimal_places=2, ) people_per_local = models.DecimalField( - verbose_name=_('people per local'), help_text=_('Estimated % people Rural'), - blank=True, null=True, - max_digits=5, decimal_places=2 + verbose_name=_("people per local"), + help_text=_("Estimated % people Rural"), + blank=True, + null=True, + max_digits=5, + decimal_places=2, ) people_targeted_with_early_actions = models.IntegerField( - verbose_name=_('people targeted with early actions'), - help_text=_('Number of persons targeted with early actions'), - blank=True, null=True + verbose_name=_("people targeted with early actions"), + help_text=_("Number of persons targeted with early actions"), + blank=True, + null=True, ) displaced_people = models.IntegerField( - verbose_name=_('displaced people'), help_text=_('Estimated number of displaced people'), - blank=True, null=True + verbose_name=_("displaced people"), help_text=_("Estimated number of displaced people"), blank=True, null=True ) operation_objective = models.TextField( - verbose_name=_('operation objective'), help_text=_('Overall objective of the operation'), - blank=True, null=True, + verbose_name=_("operation objective"), + help_text=_("Overall objective of the operation"), + blank=True, + null=True, ) response_strategy = models.TextField( - verbose_name=_('response strategy'), - blank=True, null=True, - ) - planned_interventions = models.ManyToManyField( - PlannedIntervention, - verbose_name=_('planned intervention'), blank=True - ) - did_national_society = models.BooleanField( - verbose_name=_('Did National Society'), - null=True, blank=True - ) - ns_request_date = models.DateField(verbose_name=_('ns request date'), null=True, blank=True) - submission_to_geneva = models.DateField(verbose_name=_('submission to geneva'), null=True, blank=True) - date_of_approval = models.DateField(verbose_name=_('date of approval'), null=True, blank=True) - end_date = models.DateField(verbose_name=_('end date'), null=True, blank=True) - publishing_date = models.DateField(verbose_name=_('publishing date'), null=True, blank=True) - operation_timeframe = models.IntegerField(verbose_name=_('operation timeframe'), null=True, blank=True) - appeal_code = models.CharField(verbose_name=_('appeal code'), max_length=255, null=True, blank=True) - glide_code = models.CharField(verbose_name=_('glide number'), max_length=255, null=True, blank=True) + verbose_name=_("response strategy"), + blank=True, + null=True, + ) + planned_interventions = models.ManyToManyField(PlannedIntervention, verbose_name=_("planned intervention"), blank=True) + did_national_society = models.BooleanField(verbose_name=_("Did National Society"), null=True, blank=True) + ns_request_date = models.DateField(verbose_name=_("ns request date"), null=True, blank=True) + submission_to_geneva = models.DateField(verbose_name=_("submission to geneva"), null=True, blank=True) + date_of_approval = models.DateField(verbose_name=_("date of approval"), null=True, blank=True) + end_date = models.DateField(verbose_name=_("end date"), null=True, blank=True) + publishing_date = models.DateField(verbose_name=_("publishing date"), null=True, blank=True) + operation_timeframe = models.IntegerField(verbose_name=_("operation timeframe"), null=True, blank=True) + appeal_code = models.CharField(verbose_name=_("appeal code"), max_length=255, null=True, blank=True) + glide_code = models.CharField(verbose_name=_("glide number"), max_length=255, null=True, blank=True) ifrc_appeal_manager_name = models.CharField( - verbose_name=_('ifrc appeal manager name'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc appeal manager name"), max_length=255, null=True, blank=True ) ifrc_appeal_manager_email = models.CharField( - verbose_name=_('ifrc appeal manager email'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc appeal manager email"), max_length=255, null=True, blank=True ) ifrc_appeal_manager_title = models.CharField( - verbose_name=_('ifrc appeal manager title'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc appeal manager title"), max_length=255, null=True, blank=True ) ifrc_appeal_manager_phone_number = models.CharField( - verbose_name=_('ifrc appeal manager phone number'), max_length=100, - null=True, blank=True + verbose_name=_("ifrc appeal manager phone number"), max_length=100, null=True, blank=True ) ifrc_project_manager_name = models.CharField( - verbose_name=_('ifrc project manager name'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc project manager name"), max_length=255, null=True, blank=True ) ifrc_project_manager_email = models.CharField( - verbose_name=_('ifrc project manager email'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc project manager email"), max_length=255, null=True, blank=True ) ifrc_project_manager_title = models.CharField( - verbose_name=_('ifrc project manager title'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc project manager title"), max_length=255, null=True, blank=True ) ifrc_project_manager_phone_number = models.CharField( - verbose_name=_('ifrc project manager phone number'), max_length=100, - null=True, blank=True + verbose_name=_("ifrc project manager phone number"), max_length=100, null=True, blank=True ) national_society_contact_name = models.CharField( - verbose_name=_('national society contact name'), max_length=255, - null=True, blank=True + verbose_name=_("national society contact name"), max_length=255, null=True, blank=True ) national_society_contact_email = models.CharField( - verbose_name=_('national society contact email'), max_length=255, - null=True, blank=True + verbose_name=_("national society contact email"), max_length=255, null=True, blank=True ) national_society_contact_title = models.CharField( - verbose_name=_('national society contact title'), max_length=255, - null=True, blank=True + verbose_name=_("national society contact title"), max_length=255, null=True, blank=True ) national_society_contact_phone_number = models.CharField( - verbose_name=_('national society contact phone number'), max_length=100, - null=True, blank=True - ) - media_contact_name = models.CharField( - verbose_name=_('media contact name'), max_length=255, - null=True, blank=True - ) - media_contact_email = models.CharField( - verbose_name=_('media contact email'), max_length=255, - null=True, blank=True - ) - media_contact_title = models.CharField( - verbose_name=_('media contact title'), max_length=255, - null=True, blank=True + verbose_name=_("national society contact phone number"), max_length=100, null=True, blank=True ) + media_contact_name = models.CharField(verbose_name=_("media contact name"), max_length=255, null=True, blank=True) + media_contact_email = models.CharField(verbose_name=_("media contact email"), max_length=255, null=True, blank=True) + media_contact_title = models.CharField(verbose_name=_("media contact title"), max_length=255, null=True, blank=True) media_contact_phone_number = models.CharField( - verbose_name=_('media_contact phone number'), max_length=100, - null=True, blank=True - ) - ifrc_emergency_name = models.CharField( - verbose_name=_('ifrc emergency name'), max_length=255, - null=True, blank=True - ) - ifrc_emergency_email = models.CharField( - verbose_name=_('ifrc emergency email'), max_length=255, - null=True, blank=True - ) - ifrc_emergency_title = models.CharField( - verbose_name=_('ifrc emergency title'), max_length=255, - null=True, blank=True + verbose_name=_("media_contact phone number"), max_length=100, null=True, blank=True ) + ifrc_emergency_name = models.CharField(verbose_name=_("ifrc emergency name"), max_length=255, null=True, blank=True) + ifrc_emergency_email = models.CharField(verbose_name=_("ifrc emergency email"), max_length=255, null=True, blank=True) + ifrc_emergency_title = models.CharField(verbose_name=_("ifrc emergency title"), max_length=255, null=True, blank=True) ifrc_emergency_phone_number = models.CharField( - verbose_name=_('ifrc emergency phone number'), max_length=100, - null=True, blank=True - ) - originator_name = models.CharField( - verbose_name=_('originator name'), max_length=255, - null=True, blank=True - ) - originator_email = models.CharField( - verbose_name=_('originator email'), max_length=255, - null=True, blank=True - ) - originator_title = models.CharField( - verbose_name=_('originator title'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc emergency phone number"), max_length=100, null=True, blank=True ) + originator_name = models.CharField(verbose_name=_("originator name"), max_length=255, null=True, blank=True) + originator_email = models.CharField(verbose_name=_("originator email"), max_length=255, null=True, blank=True) + originator_title = models.CharField(verbose_name=_("originator title"), max_length=255, null=True, blank=True) originator_phone_number = models.CharField( - verbose_name=_('originator phone number'), max_length=100, - null=True, blank=True + verbose_name=_("originator phone number"), max_length=100, null=True, blank=True ) human_resource = models.TextField( - blank=True, null=True, - verbose_name=_('human resource'), - help_text=_('how many volunteers and staff involved in the response?') - ) - is_surge_personnel_deployed = models.BooleanField( - blank=True, null=True, - verbose_name=_('Is surge personnel deployed') + blank=True, + null=True, + verbose_name=_("human resource"), + help_text=_("how many volunteers and staff involved in the response?"), ) + is_surge_personnel_deployed = models.BooleanField(blank=True, null=True, verbose_name=_("Is surge personnel deployed")) surge_personnel_deployed = models.TextField( - blank=True, null=True, - verbose_name=_('surge personnel deployed'), - help_text=_('Will a Surge personnel be deployed?') + blank=True, null=True, verbose_name=_("surge personnel deployed"), help_text=_("Will a Surge personnel be deployed?") ) logistic_capacity_of_ns = models.TextField( - blank=True, null=True, - verbose_name=_('logistic capacity of ns'), - help_text=_('what is the logistics capacity of the National Society?') + blank=True, + null=True, + verbose_name=_("logistic capacity of ns"), + help_text=_("what is the logistics capacity of the National Society?"), ) safety_concerns = models.TextField( - blank=True, null=True, - verbose_name=_('safety concerns'), - help_text=_('Are there any safety/security concerns which may impact the implementation of this operation?') - ) - pmer = models.TextField( - blank=True, null=True, - verbose_name=_('pmer'), - help_text=_('Does the NS have PMER capacity?') + blank=True, + null=True, + verbose_name=_("safety concerns"), + help_text=_("Are there any safety/security concerns which may impact the implementation of this operation?"), ) + pmer = models.TextField(blank=True, null=True, verbose_name=_("pmer"), help_text=_("Does the NS have PMER capacity?")) communication = models.TextField( - blank=True, null=True, - verbose_name=_('organization'), - help_text=_('Does the NS have Communications capacity?') + blank=True, null=True, verbose_name=_("organization"), help_text=_("Does the NS have Communications capacity?") ) event_map = models.ForeignKey( - 'DrefFile', on_delete=models.SET_NULL, - null=True, blank=True, - verbose_name=_('event map'), - related_name='event_map_dref' + "DrefFile", + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name=_("event map"), + related_name="event_map_dref", ) images = models.ManyToManyField( # null=True is not ok here: null has no effect on ManyToManyField. - 'DrefFile', blank=True, - verbose_name=_('images'), - related_name='image_dref' + "DrefFile", + blank=True, + verbose_name=_("images"), + related_name="image_dref", ) budget_file = models.ForeignKey( - 'DrefFile', on_delete=models.SET_NULL, - null=True, blank=True, - verbose_name=_('budget file'), - related_name='budget_file_dref' + "DrefFile", + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name=_("budget file"), + related_name="budget_file_dref", ) budget_file_preview = models.FileField( - verbose_name=_('budget file preview'), - null=True, blank=True, - upload_to='dref/images/' + verbose_name=_("budget file preview"), null=True, blank=True, upload_to="dref/images/" ) assessment_report = models.ForeignKey( - 'DrefFile', on_delete=models.SET_NULL, - null=True, blank=True, - verbose_name=_('Assessment Report'), - related_name='dref_assessment_report' + "DrefFile", + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name=_("Assessment Report"), + related_name="dref_assessment_report", ) supporting_document = models.ForeignKey( - 'DrefFile', on_delete=models.SET_NULL, - null=True, blank=True, - verbose_name=_('Supporting Document'), - related_name='dref_supporting_document' + "DrefFile", + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name=_("Supporting Document"), + related_name="dref_supporting_document", ) cover_image = models.ForeignKey( - 'DrefFile', on_delete=models.SET_NULL, - null=True, blank=True, - verbose_name=_('cover image'), - related_name='cover_image_dref' + "DrefFile", + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name=_("cover image"), + related_name="cover_image_dref", ) is_published = models.BooleanField( default=False, - verbose_name=_('Is published'), + verbose_name=_("Is published"), ) is_final_report_created = models.BooleanField( default=False, - verbose_name=_('Is final report created'), + verbose_name=_("Is final report created"), ) country = models.ForeignKey( - Country, verbose_name=_('country'), + Country, + verbose_name=_("country"), on_delete=models.CASCADE, - help_text=_('Affected County'), - null=True, blank=True, - related_name='dref_country' - ) - district = models.ManyToManyField( - District, blank=True, - verbose_name=_('district') - ) - risk_security = models.ManyToManyField( - RiskSecurity, blank=True, - verbose_name=_('Risk Security') - ) - risk_security_concern = models.TextField( - blank=True, null=True, - verbose_name=_('Risk Security Concern') - ) - is_man_made_event = models.BooleanField( - verbose_name=_('Is Man-made Event'), - null=True, blank=True - ) - is_assessment_report = models.BooleanField( - verbose_name=_('Is assessment Report'), - default=False + help_text=_("Affected County"), + null=True, + blank=True, + related_name="dref_country", ) + district = models.ManyToManyField(District, blank=True, verbose_name=_("district")) + risk_security = models.ManyToManyField(RiskSecurity, blank=True, verbose_name=_("Risk Security")) + risk_security_concern = models.TextField(blank=True, null=True, verbose_name=_("Risk Security Concern")) + is_man_made_event = models.BooleanField(verbose_name=_("Is Man-made Event"), null=True, blank=True) + is_assessment_report = models.BooleanField(verbose_name=_("Is assessment Report"), default=False) __budget_file_id = None class Meta: - verbose_name = _('dref') - verbose_name_plural = _('drefs') + verbose_name = _("dref") + verbose_name_plural = _("drefs") def save(self, *args, **kwargs): if self.budget_file and self.budget_file_id != self.__budget_file_id: @@ -647,36 +575,67 @@ def save(self, *args, **kwargs): if len(pages) > 0: budget_file_preview = pages[0] # get first page filename = f'preview_{self.budget_file.file.name.split("/")[0]}.png' - temp_image = open(os.path.join('/tmp', filename), 'wb') - budget_file_preview.save(temp_image, 'PNG') - thumb_data = open(os.path.join('/tmp', filename), 'rb') + temp_image = open(os.path.join("/tmp", filename), "wb") + budget_file_preview.save(temp_image, "PNG") + thumb_data = open(os.path.join("/tmp", filename), "rb") self.budget_file_preview.save(filename, thumb_data, save=False) else: - raise ValidationError({'budget_file': 'Sorry cannot generate preview for empty pdf'}) + raise ValidationError({"budget_file": "Sorry cannot generate preview for empty pdf"}) self.status = Dref.Status.COMPLETED if self.date_of_approval else Dref.Status.IN_PROGRESS self.__budget_file_id = self.budget_file_id super().save(*args, **kwargs) def __str__(self): - return f'{self.title} – {self.created_at.date()}, {self.get_status_display()}' + return f"{self.title} – {self.created_at.date()}, {self.get_status_display()}" + + @staticmethod + def get_for(user): + user_id = user.id + current_user_list = [] + current_user_list.append(user_id) + return Dref.objects.annotate( + created_user_list=models.F("created_by"), + users_list=ArrayAgg("users", filter=models.Q(users__isnull=False)), + op_users=models.Subquery( + DrefOperationalUpdate.objects.filter(dref=models.OuterRef("id")) + .order_by() + .values("id") + .annotate(c=ArrayAgg("users", filter=models.Q(users__isnull=False))) + .values("c")[:1] + ), + fr_users=models.Subquery( + DrefFinalReport.objects.filter(dref=models.OuterRef("id")) + .order_by() + .values("id") + .annotate(c=ArrayAgg("users", filter=models.Q(users__isnull=False))) + .values("c")[:1], + ), + ).filter( + models.Q(created_user_list=user_id) + | models.Q(users_list__contains=current_user_list) + | models.Q(op_users__contains=current_user_list) + | models.Q(fr_users__contains=current_user_list) + ).distinct() class DrefFile(models.Model): file = models.FileField( - verbose_name=_('file'), - upload_to='dref/images/', + verbose_name=_("file"), + upload_to="dref/images/", ) created_by = models.ForeignKey( - settings.AUTH_USER_MODEL, verbose_name=_('created_by'), - on_delete=models.SET_NULL, null=True, + settings.AUTH_USER_MODEL, + verbose_name=_("created_by"), + on_delete=models.SET_NULL, + null=True, ) caption = models.CharField(max_length=225, blank=True, null=True) client_id = models.CharField(max_length=50, null=True, blank=True) class Meta: - verbose_name = _('dref file') - verbose_name_plural = _('dref files') + verbose_name = _("dref file") + verbose_name_plural = _("dref files") def clone(self, user): clone = copy.deepcopy(self) @@ -688,806 +647,605 @@ def clone(self, user): class DrefOperationalUpdate(models.Model): - created_at = models.DateTimeField(verbose_name=_('created at'), auto_now_add=True) - modified_at = models.DateTimeField(verbose_name=_('modified at'), auto_now=True) + created_at = models.DateTimeField(verbose_name=_("created at"), auto_now_add=True) + modified_at = models.DateTimeField(verbose_name=_("modified at"), auto_now=True, blank=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, - verbose_name=_('created by'), + verbose_name=_("created by"), on_delete=models.SET_NULL, null=True, - related_name='created_by_dref_operational_update' + related_name="created_by_dref_operational_update", ) modified_by = models.ForeignKey( settings.AUTH_USER_MODEL, - verbose_name=_('modified by'), + verbose_name=_("modified by"), on_delete=models.SET_NULL, null=True, - related_name='modified_by_dref_operational_update' - ) - dref = models.ForeignKey( - Dref, verbose_name=_('Dref'), - on_delete=models.CASCADE + related_name="modified_by_dref_operational_update", ) + dref = models.ForeignKey(Dref, verbose_name=_("Dref"), on_delete=models.CASCADE) title = models.CharField( - verbose_name=_('title'), - null=True, blank=True, + verbose_name=_("title"), + null=True, + blank=True, max_length=255, ) - title_prefix = models.CharField( - verbose_name=_('title prefix'), max_length=255, - null=True, blank=True - ) + title_prefix = models.CharField(verbose_name=_("title prefix"), max_length=255, null=True, blank=True) national_society = models.ForeignKey( - Country, verbose_name=_('national_society'), - null=True, blank=True, + Country, + verbose_name=_("national_society"), + null=True, + blank=True, on_delete=models.CASCADE, - related_name='national_society_operational_update' + related_name="national_society_operational_update", ) disaster_type = models.ForeignKey( - DisasterType, verbose_name=_('disaster type'), - blank=True, null=True, - on_delete=models.SET_NULL - ) - type_of_onset = models.IntegerField( - choices=Dref.OnsetType.choices, - verbose_name=_('onset type'), - null=True, blank=True + DisasterType, verbose_name=_("disaster type"), blank=True, null=True, on_delete=models.SET_NULL ) + type_of_dref = models.IntegerField(choices=Dref.DrefType.choices, verbose_name=_("dref type"), null=True, blank=True) + type_of_onset = models.IntegerField(choices=Dref.OnsetType.choices, verbose_name=_("onset type"), null=True, blank=True) disaster_category = models.IntegerField( - choices=Dref.DisasterCategory.choices, - verbose_name=_('disaster category'), - null=True, blank=True - ) - number_of_people_targeted = models.IntegerField( - verbose_name=_('Number of people targeted'), - blank=True, null=True - ) - number_of_people_affected = models.IntegerField( - verbose_name=_('number of people affected'), - blank=True, null=True - ) - dref_allocated_so_far = models.IntegerField( - verbose_name=_('Dref allocated so far'), - null=True, blank=True - ) - additional_allocation = models.IntegerField( - verbose_name=_('Additional allocation'), - null=True, blank=True - ) - total_dref_allocation = models.IntegerField( - verbose_name=_('Total dref allocation'), - null=True, blank=True - ) - emergency_appeal_planned = models.BooleanField( - verbose_name=_('emergency appeal planned '), - null=True, blank=True - ) + choices=Dref.DisasterCategory.choices, verbose_name=_("disaster category"), null=True, blank=True + ) + number_of_people_targeted = models.IntegerField(verbose_name=_("Number of people targeted"), blank=True, null=True) + number_of_people_affected = models.IntegerField(verbose_name=_("number of people affected"), blank=True, null=True) + dref_allocated_so_far = models.IntegerField(verbose_name=_("Dref allocated so far"), null=True, blank=True) + additional_allocation = models.IntegerField(verbose_name=_("Additional allocation"), null=True, blank=True) + total_dref_allocation = models.IntegerField(verbose_name=_("Total dref allocation"), null=True, blank=True) + emergency_appeal_planned = models.BooleanField(verbose_name=_("emergency appeal planned "), null=True, blank=True) event_map = models.ForeignKey( - 'DrefFile', on_delete=models.SET_NULL, - null=True, blank=True, - verbose_name=_('event map'), - related_name='event_map_dref_operational_update' + "DrefFile", + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name=_("event map"), + related_name="event_map_dref_operational_update", ) images = models.ManyToManyField( - 'DrefFile', blank=True, - verbose_name=_('images'), - related_name='image_dref_operational_update' + "DrefFile", blank=True, verbose_name=_("images"), related_name="image_dref_operational_update" ) cover_image = models.ForeignKey( - 'DrefFile', on_delete=models.SET_NULL, - blank=True, null=True, - verbose_name=_('cover image'), - related_name='cover_image_dref_operational_update' + "DrefFile", + on_delete=models.SET_NULL, + blank=True, + null=True, + verbose_name=_("cover image"), + related_name="cover_image_dref_operational_update", ) budget_file = models.ForeignKey( - 'DrefFile', on_delete=models.SET_NULL, - blank=True, null=True, - verbose_name=_('budget file'), - related_name='budget_file_dref_operational_update' + "DrefFile", + on_delete=models.SET_NULL, + blank=True, + null=True, + verbose_name=_("budget file"), + related_name="budget_file_dref_operational_update", ) assessment_report = models.ForeignKey( - 'DrefFile', on_delete=models.SET_NULL, - null=True, blank=True, - verbose_name=_('Assessment Report'), - related_name='dref_operational_update_assessment_report' + "DrefFile", + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name=_("Assessment Report"), + related_name="dref_operational_update_assessment_report", ) photos = models.ManyToManyField( - 'DrefFile', blank=True, - verbose_name=_('images'), - related_name='photos_dref_operational_update' - ) - operational_update_number = models.IntegerField( - verbose_name=_('Operational Update Number'), - null=True, blank=True - ) - reporting_timeframe = models.DateField( - verbose_name=_('Reporting Timeframe'), - null=True, blank=True + "DrefFile", blank=True, verbose_name=_("images"), related_name="photos_dref_operational_update" ) + operational_update_number = models.IntegerField(verbose_name=_("Operational Update Number"), null=True, blank=True) + reporting_timeframe = models.DateField(verbose_name=_("Reporting Timeframe"), null=True, blank=True) update_date = models.DateField( - null=True, blank=True, - verbose_name=_('Update Date'), + null=True, + blank=True, + verbose_name=_("Update Date"), ) is_timeframe_extension_required = models.BooleanField( - null=True, blank=True, - verbose_name=_('Is Timeframe Extension Required') - ) - new_operational_start_date = models.DateField( - verbose_name=_('New Operation Start Date'), - null=True, blank=True - ) - new_operational_end_date = models.DateField( - verbose_name=_('New Operation End Date'), - null=True, blank=True - ) - total_operation_timeframe = models.IntegerField( - verbose_name=_('Total Operation Timeframe'), - null=True, blank=True - ) - appeal_code = models.CharField( - verbose_name=_('appeal code'), - max_length=255, - null=True, blank=True - ) - glide_code = models.CharField( - verbose_name=_('glide number'), - max_length=255, - null=True, blank=True + null=True, blank=True, verbose_name=_("Is Timeframe Extension Required") ) + new_operational_start_date = models.DateField(verbose_name=_("New Operation Start Date"), null=True, blank=True) + new_operational_end_date = models.DateField(verbose_name=_("New Operation End Date"), null=True, blank=True) + total_operation_timeframe = models.IntegerField(verbose_name=_("Total Operation Timeframe"), null=True, blank=True) + appeal_code = models.CharField(verbose_name=_("appeal code"), max_length=255, null=True, blank=True) + glide_code = models.CharField(verbose_name=_("glide number"), max_length=255, null=True, blank=True) ifrc_appeal_manager_name = models.CharField( - verbose_name=_('ifrc appeal manager name'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc appeal manager name"), max_length=255, null=True, blank=True ) ifrc_appeal_manager_email = models.CharField( - verbose_name=_('ifrc appeal manager email'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc appeal manager email"), max_length=255, null=True, blank=True ) ifrc_appeal_manager_title = models.CharField( - verbose_name=_('ifrc appeal manager title'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc appeal manager title"), max_length=255, null=True, blank=True ) ifrc_appeal_manager_phone_number = models.CharField( - verbose_name=_('ifrc appeal manager phone number'), max_length=100, - null=True, blank=True + verbose_name=_("ifrc appeal manager phone number"), max_length=100, null=True, blank=True ) ifrc_project_manager_name = models.CharField( - verbose_name=_('ifrc project manager name'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc project manager name"), max_length=255, null=True, blank=True ) ifrc_project_manager_email = models.CharField( - verbose_name=_('ifrc project manager email'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc project manager email"), max_length=255, null=True, blank=True ) ifrc_project_manager_title = models.CharField( - verbose_name=_('ifrc project manager title'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc project manager title"), max_length=255, null=True, blank=True ) ifrc_project_manager_phone_number = models.CharField( - verbose_name=_('ifrc project manager phone number'), max_length=100, - null=True, blank=True + verbose_name=_("ifrc project manager phone number"), max_length=100, null=True, blank=True ) national_society_contact_name = models.CharField( - verbose_name=_('national society contact name'), max_length=255, - null=True, blank=True + verbose_name=_("national society contact name"), max_length=255, null=True, blank=True ) national_society_contact_email = models.CharField( - verbose_name=_('national society contact email'), max_length=255, - null=True, blank=True + verbose_name=_("national society contact email"), max_length=255, null=True, blank=True ) national_society_contact_title = models.CharField( - verbose_name=_('national society contact title'), max_length=255, - null=True, blank=True + verbose_name=_("national society contact title"), max_length=255, null=True, blank=True ) national_society_contact_phone_number = models.CharField( - verbose_name=_('national society contact phone number'), max_length=100, - null=True, blank=True - ) - media_contact_name = models.CharField( - verbose_name=_('media contact name'), max_length=255, - null=True, blank=True - ) - media_contact_email = models.CharField( - verbose_name=_('media contact email'), max_length=255, - null=True, blank=True - ) - media_contact_title = models.CharField( - verbose_name=_('media contact title'), max_length=255, - null=True, blank=True + verbose_name=_("national society contact phone number"), max_length=100, null=True, blank=True ) + media_contact_name = models.CharField(verbose_name=_("media contact name"), max_length=255, null=True, blank=True) + media_contact_email = models.CharField(verbose_name=_("media contact email"), max_length=255, null=True, blank=True) + media_contact_title = models.CharField(verbose_name=_("media contact title"), max_length=255, null=True, blank=True) media_contact_phone_number = models.CharField( - verbose_name=_('media_contact phone number'), max_length=100, - null=True, blank=True - ) - ifrc_emergency_name = models.CharField( - verbose_name=_('ifrc emergency name'), max_length=255, - null=True, blank=True - ) - ifrc_emergency_email = models.CharField( - verbose_name=_('ifrc emergency email'), max_length=255, - null=True, blank=True - ) - ifrc_emergency_title = models.CharField( - verbose_name=_('ifrc emergency title'), max_length=255, - null=True, blank=True + verbose_name=_("media_contact phone number"), max_length=100, null=True, blank=True ) + ifrc_emergency_name = models.CharField(verbose_name=_("ifrc emergency name"), max_length=255, null=True, blank=True) + ifrc_emergency_email = models.CharField(verbose_name=_("ifrc emergency email"), max_length=255, null=True, blank=True) + ifrc_emergency_title = models.CharField(verbose_name=_("ifrc emergency title"), max_length=255, null=True, blank=True) ifrc_emergency_phone_number = models.CharField( - verbose_name=_('ifrc emergency phone number'), max_length=100, - null=True, blank=True - ) - changing_timeframe_operation = models.BooleanField( - null=True, blank=True, verbose_name=_('Changing time operation') - ) - changing_operation_strategy = models.BooleanField( - null=True, blank=True, verbose_name=_('Changing operation strategy') + verbose_name=_("ifrc emergency phone number"), max_length=100, null=True, blank=True ) + changing_timeframe_operation = models.BooleanField(null=True, blank=True, verbose_name=_("Changing time operation")) + changing_operation_strategy = models.BooleanField(null=True, blank=True, verbose_name=_("Changing operation strategy")) changing_target_population_of_operation = models.BooleanField( - null=True, blank=True, verbose_name=_('Changing target population of operation') - ) - changing_geographic_location = models.BooleanField( - null=True, blank=True, verbose_name=_('Changing geographic location') - ) - changing_budget = models.BooleanField( - null=True, blank=True, verbose_name=_('Changing budget') + null=True, blank=True, verbose_name=_("Changing target population of operation") ) + changing_geographic_location = models.BooleanField(null=True, blank=True, verbose_name=_("Changing geographic location")) + changing_budget = models.BooleanField(null=True, blank=True, verbose_name=_("Changing budget")) request_for_second_allocation = models.BooleanField( - null=True, blank=True, verbose_name=_('Request for second allocation') - ) - summary_of_change = models.TextField( - verbose_name=_('Summary of change'), - null=True, blank=True - ) - has_change_since_request = models.BooleanField( - verbose_name=_('Has change since request'), - null=True, blank=True - ) - event_description = models.TextField( - verbose_name=_('Event description'), - null=True, blank=True - ) - anticipatory_actions = models.TextField( - verbose_name=_('Anticipatory actions'), - null=True, blank=True - ) - event_scope = models.TextField( - verbose_name=_('Event scope'), - null=True, blank=True + null=True, blank=True, verbose_name=_("Request for second allocation") ) + summary_of_change = models.TextField(verbose_name=_("Summary of change"), null=True, blank=True) + has_change_since_request = models.BooleanField(verbose_name=_("Has change since request"), null=True, blank=True) + event_description = models.TextField(verbose_name=_("Event description"), null=True, blank=True) + anticipatory_actions = models.TextField(verbose_name=_("Anticipatory actions"), null=True, blank=True) + event_scope = models.TextField(verbose_name=_("Event scope"), null=True, blank=True) national_society_actions = models.ManyToManyField( - NationalSocietyAction, - verbose_name=_('national society actions'), - blank=True - ) - ifrc = models.TextField( - verbose_name=_('ifrc'), - blank=True, null=True - ) - icrc = models.TextField( - verbose_name=_('icrc'), - blank=True, null=True - ) - partner_national_society = models.TextField( - verbose_name=_('partner national society'), - blank=True, null=True + NationalSocietyAction, verbose_name=_("national society actions"), blank=True ) + ifrc = models.TextField(verbose_name=_("ifrc"), blank=True, null=True) + icrc = models.TextField(verbose_name=_("icrc"), blank=True, null=True) + partner_national_society = models.TextField(verbose_name=_("partner national society"), blank=True, null=True) government_requested_assistance = models.BooleanField( - null=True, blank=True, - help_text=_('Has government requested assistance') - ) - national_authorities = models.TextField( - verbose_name=_('national authorities'), - blank=True, null=True - ) - is_there_un_or_other_actor = models.BooleanField( - null=True, blank=True, - verbose_name=_('Is there un_or_other_actor') - ) - un_or_other_actor = models.TextField( - verbose_name=_('un or other'), - blank=True, null=True + null=True, blank=True, help_text=_("Has government requested assistance") ) + national_authorities = models.TextField(verbose_name=_("national authorities"), blank=True, null=True) + is_there_un_or_other_actor = models.BooleanField(null=True, blank=True, verbose_name=_("Is there un_or_other_actor")) + un_or_other_actor = models.TextField(verbose_name=_("un or other"), blank=True, null=True) is_there_major_coordination_mechanism = models.BooleanField( - null=True, blank=True, - help_text=_('Is there major coordinate mechanism') + null=True, blank=True, help_text=_("Is there major coordinate mechanism") ) major_coordination_mechanism = models.TextField( - blank=True, null=True, - verbose_name=_('major coordination mechanism'), - ) - needs_identified = models.ManyToManyField( - IdentifiedNeed, verbose_name=_('needs identified'), - blank=True + blank=True, + null=True, + verbose_name=_("major coordination mechanism"), ) - people_assisted = models.TextField(verbose_name=_('people assisted'), blank=True, null=True) + needs_identified = models.ManyToManyField(IdentifiedNeed, verbose_name=_("needs identified"), blank=True) + people_assisted = models.TextField(verbose_name=_("people assisted"), blank=True, null=True) selection_criteria = models.TextField( - verbose_name=_('selection criteria'), blank=True, null=True, - help_text=_('Selection criteria for affected people') + verbose_name=_("selection criteria"), blank=True, null=True, help_text=_("Selection criteria for affected people") ) community_involved = models.TextField( - verbose_name=_('community involved'), blank=True, null=True, - help_text=_('Community been involved in the analysis of the process') + verbose_name=_("community involved"), + blank=True, + null=True, + help_text=_("Community been involved in the analysis of the process"), ) entity_affected = models.TextField( - verbose_name=_('entity affected'), blank=True, null=True, - help_text=_('Protection, gender, Inclusion affected in this process') - ) - women = models.IntegerField(verbose_name=_('women'), blank=True, null=True) - men = models.IntegerField(verbose_name=_('men'), blank=True, null=True) - girls = models.IntegerField( - verbose_name=_('girls'), help_text=_('Girls under 18'), - blank=True, null=True - ) - boys = models.IntegerField( - verbose_name=_('boys'), help_text=_('Boys under 18'), - blank=True, null=True + verbose_name=_("entity affected"), + blank=True, + null=True, + help_text=_("Protection, gender, Inclusion affected in this process"), ) + women = models.IntegerField(verbose_name=_("women"), blank=True, null=True) + men = models.IntegerField(verbose_name=_("men"), blank=True, null=True) + girls = models.IntegerField(verbose_name=_("girls"), help_text=_("Girls under 18"), blank=True, null=True) + boys = models.IntegerField(verbose_name=_("boys"), help_text=_("Boys under 18"), blank=True, null=True) disability_people_per = models.DecimalField( - verbose_name=_('disability people per'), - blank=True, null=True, - max_digits=5, decimal_places=2 + verbose_name=_("disability people per"), blank=True, null=True, max_digits=5, decimal_places=2 ) people_per_urban = models.DecimalField( - verbose_name=_('people per urban'), - blank=True, null=True, - max_digits=5, decimal_places=2 + verbose_name=_("people per urban"), blank=True, null=True, max_digits=5, decimal_places=2 ) people_per_local = models.DecimalField( - verbose_name=_('people per local'), - blank=True, null=True, - max_digits=5, decimal_places=2 + verbose_name=_("people per local"), blank=True, null=True, max_digits=5, decimal_places=2 ) people_targeted_with_early_actions = models.IntegerField( - verbose_name=_('people targeted with early actions'), - blank=True, null=True - ) - displaced_people = models.IntegerField( - verbose_name=_('displaced people'), - blank=True, null=True + verbose_name=_("people targeted with early actions"), blank=True, null=True ) + displaced_people = models.IntegerField(verbose_name=_("displaced people"), blank=True, null=True) operation_objective = models.TextField( - verbose_name=_('operation objective'), - blank=True, null=True, + verbose_name=_("operation objective"), + blank=True, + null=True, ) response_strategy = models.TextField( - verbose_name=_('response strategy'), - blank=True, null=True, - ) - planned_interventions = models.ManyToManyField( - PlannedIntervention, - verbose_name=_('planned intervention'), - blank=True + verbose_name=_("response strategy"), + blank=True, + null=True, ) + planned_interventions = models.ManyToManyField(PlannedIntervention, verbose_name=_("planned intervention"), blank=True) is_published = models.BooleanField( default=False, - verbose_name=_('Is published'), + verbose_name=_("Is published"), ) country = models.ForeignKey( - Country, verbose_name=_('country'), + Country, + verbose_name=_("country"), on_delete=models.CASCADE, - help_text=_('Affected County'), - null=True, blank=True, - related_name='operational_update_country' - ) - district = models.ManyToManyField( - District, blank=True, - verbose_name=_('district') + help_text=_("Affected County"), + null=True, + blank=True, + related_name="operational_update_country", ) + district = models.ManyToManyField(District, blank=True, verbose_name=_("district")) users = models.ManyToManyField( - settings.AUTH_USER_MODEL, verbose_name=_('users'), - blank=True, related_name='user_dref_operational_update' - ) - risk_security = models.ManyToManyField( - RiskSecurity, blank=True, - verbose_name=_('Risk Security') - ) - risk_security_concern = models.TextField( - blank=True, null=True, - verbose_name=_('Risk Security Concern') + settings.AUTH_USER_MODEL, verbose_name=_("users"), blank=True, related_name="user_dref_operational_update" ) + risk_security = models.ManyToManyField(RiskSecurity, blank=True, verbose_name=_("Risk Security")) + risk_security_concern = models.TextField(blank=True, null=True, verbose_name=_("Risk Security Concern")) has_forecasted_event_materialize = models.BooleanField( - verbose_name=_('Has Forecasted Event Materialize'), - null=True, blank=True + verbose_name=_("Has Forecasted Event Materialize"), null=True, blank=True ) anticipatory_to_response = models.TextField( - verbose_name=_('Please explain how is the operation is transitioning from Anticipatory to Response'), - null=True, blank=True - ) - specified_trigger_met = models.TextField( - verbose_name=_('Specified Trigger Met'), - null=True, blank=True - ) - is_assessment_report = models.BooleanField( - verbose_name=_('Is assessment Report'), - null=True, blank=True - ) - people_in_need = models.IntegerField( - verbose_name=_('people in need'), - blank=True, null=True + verbose_name=_("Please explain how is the operation is transitioning from Anticipatory to Response"), + null=True, + blank=True, ) + specified_trigger_met = models.TextField(verbose_name=_("Specified Trigger Met"), null=True, blank=True) + is_assessment_report = models.BooleanField(verbose_name=_("Is assessment Report"), null=True, blank=True) + people_in_need = models.IntegerField(verbose_name=_("people in need"), blank=True, null=True) event_date = models.DateField( - verbose_name=_('event date'), - null=True, blank=True, + verbose_name=_("event date"), + null=True, + blank=True, ) ns_respond_date = models.DateField( - verbose_name=_('ns respond date'), - null=True, blank=True, - ) - ns_respond = models.BooleanField( - null=True, blank=True, - default=False, help_text=_('Did NS respond') - ) - total_targeted_population = models.IntegerField( - verbose_name=_('total targeted population'), - blank=True, null=True - ) - has_event_occurred = models.BooleanField( - null=True, blank=True, - help_text=_('Has Event occurred') - ) - reporting_start_date = models.DateField( - verbose_name=_('Reporting Time Start Date'), - null=True, blank=True - ) - reporting_end_date = models.DateField( - verbose_name=_('Reporting Time End Date'), - null=True, blank=True + verbose_name=_("ns respond date"), + null=True, + blank=True, ) + ns_respond = models.BooleanField(null=True, blank=True, default=False, help_text=_("Did NS respond")) + total_targeted_population = models.IntegerField(verbose_name=_("total targeted population"), blank=True, null=True) + has_event_occurred = models.BooleanField(null=True, blank=True, help_text=_("Has Event occurred")) + reporting_start_date = models.DateField(verbose_name=_("Reporting Time Start Date"), null=True, blank=True) + reporting_end_date = models.DateField(verbose_name=_("Reporting Time End Date"), null=True, blank=True) human_resource = models.TextField( - blank=True, null=True, - verbose_name=_('human resource'), - help_text=_('how many volunteers and staff involved in the response?') - ) - is_surge_personnel_deployed = models.BooleanField( - blank=True, null=True, - verbose_name=_('Is surge personnel deployed') + blank=True, + null=True, + verbose_name=_("human resource"), + help_text=_("how many volunteers and staff involved in the response?"), ) + is_surge_personnel_deployed = models.BooleanField(blank=True, null=True, verbose_name=_("Is surge personnel deployed")) surge_personnel_deployed = models.TextField( - blank=True, null=True, - verbose_name=_('surge personnel deployed'), - help_text=_('Will a Surge personnel be deployed?') + blank=True, null=True, verbose_name=_("surge personnel deployed"), help_text=_("Will a Surge personnel be deployed?") ) logistic_capacity_of_ns = models.TextField( - blank=True, null=True, - verbose_name=_('logistic capacity of ns'), - help_text=_('what is the logistics capacity of the National Society?') + blank=True, + null=True, + verbose_name=_("logistic capacity of ns"), + help_text=_("what is the logistics capacity of the National Society?"), ) safety_concerns = models.TextField( - blank=True, null=True, - verbose_name=_('safety concerns'), - help_text=_('Are there any safety/security concerns which may impact the implementation of this operation?') - ) - pmer = models.TextField( - blank=True, null=True, - verbose_name=_('pmer'), - help_text=_('Does the NS have PMER capacity?') + blank=True, + null=True, + verbose_name=_("safety concerns"), + help_text=_("Are there any safety/security concerns which may impact the implementation of this operation?"), ) + pmer = models.TextField(blank=True, null=True, verbose_name=_("pmer"), help_text=_("Does the NS have PMER capacity?")) communication = models.TextField( - blank=True, null=True, - verbose_name=_('organization'), - help_text=_('Does the NS have Communications capacity?') + blank=True, null=True, verbose_name=_("organization"), help_text=_("Does the NS have Communications capacity?") ) class Meta: - verbose_name = _('Dref Operational Update') - verbose_name_plural = _('Dref Operational Updates') + verbose_name = _("Dref Operational Update") + verbose_name_plural = _("Dref Operational Updates") + + @staticmethod + def get_for(user): + # get the user in dref + from dref.utils import get_dref_users + + dref_users = get_dref_users() + current_user_dref = [] + for dref in dref_users: + for dref_user in dref["users"]: + if user.id == dref_user: + current_user_dref.append(dref) + current_user_dref_id = [] + for dref in current_user_dref: + current_user_dref_id.append(dref["id"]) + op_update_users = DrefOperationalUpdate.objects.filter(dref__in=current_user_dref_id).distinct() + op_update_created_by = DrefOperationalUpdate.objects.filter(created_by=user).distinct() + union_query = op_update_users.union(op_update_created_by) + return DrefOperationalUpdate.objects.filter(id__in=union_query.values("id")).distinct() class DrefFinalReport(models.Model): - created_at = models.DateTimeField(verbose_name=_('created at'), auto_now_add=True) - modified_at = models.DateTimeField(verbose_name=_('modified at'), auto_now=True) + created_at = models.DateTimeField(verbose_name=_("created at"), auto_now_add=True) + modified_at = models.DateTimeField(verbose_name=_("modified at"), auto_now=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, - verbose_name=_('created by'), + verbose_name=_("created by"), on_delete=models.SET_NULL, null=True, - related_name='created_by_dref_final_report' + related_name="created_by_dref_final_report", ) modified_by = models.ForeignKey( settings.AUTH_USER_MODEL, - verbose_name=_('modified by'), + verbose_name=_("modified by"), on_delete=models.SET_NULL, null=True, - related_name='modified_by_dref_final_report' - ) - dref = models.OneToOneField( - Dref, verbose_name=_('Dref'), - on_delete=models.CASCADE + related_name="modified_by_dref_final_report", ) + dref = models.OneToOneField(Dref, verbose_name=_("Dref"), on_delete=models.CASCADE) title = models.CharField( - verbose_name=_('title'), - null=True, blank=True, + verbose_name=_("title"), + null=True, + blank=True, max_length=255, ) - title_prefix = models.CharField( - verbose_name=_('title prefix'), max_length=255, - null=True, blank=True - ) + title_prefix = models.CharField(verbose_name=_("title prefix"), max_length=255, null=True, blank=True) national_society = models.ForeignKey( - Country, verbose_name=_('national_society'), - null=True, blank=True, + Country, + verbose_name=_("national_society"), + null=True, + blank=True, on_delete=models.CASCADE, - related_name='national_society_final_report' + related_name="national_society_final_report", ) disaster_type = models.ForeignKey( - DisasterType, verbose_name=_('disaster type'), - blank=True, null=True, - on_delete=models.SET_NULL - ) - type_of_onset = models.IntegerField( - choices=Dref.OnsetType.choices, - verbose_name=_('onset type'), - null=True, blank=True + DisasterType, verbose_name=_("disaster type"), blank=True, null=True, on_delete=models.SET_NULL ) + type_of_dref = models.IntegerField(choices=Dref.DrefType.choices, verbose_name=_("dref type"), null=True, blank=True) + type_of_onset = models.IntegerField(choices=Dref.OnsetType.choices, verbose_name=_("onset type"), null=True, blank=True) disaster_category = models.IntegerField( - choices=Dref.DisasterCategory.choices, - verbose_name=_('disaster category'), - null=True, blank=True - ) - number_of_people_targeted = models.IntegerField( - verbose_name=_('Number of people targeted'), - blank=True, null=True - ) - number_of_people_affected = models.IntegerField( - verbose_name=_('number of people affected'), - blank=True, null=True - ) - total_dref_allocation = models.IntegerField( - verbose_name=_('Total dref allocation'), - null=True, blank=True - ) - date_of_publication = models.DateField( - verbose_name=_('Date of publication'), - blank=True, null=True - ) - total_operation_timeframe = models.IntegerField( - verbose_name=_('Total Operation Timeframe'), - null=True, blank=True - ) - operation_start_date = models.DateField( - verbose_name=_('Operation Start Date'), - null=True, blank=True - ) - appeal_code = models.CharField( - verbose_name=_('appeal code'), - max_length=255, - null=True, blank=True - ) - glide_code = models.CharField( - verbose_name=_('glide number'), - max_length=255, - null=True, blank=True - ) + choices=Dref.DisasterCategory.choices, verbose_name=_("disaster category"), null=True, blank=True + ) + number_of_people_targeted = models.IntegerField(verbose_name=_("Number of people targeted"), blank=True, null=True) + number_of_people_affected = models.IntegerField(verbose_name=_("number of people affected"), blank=True, null=True) + total_dref_allocation = models.IntegerField(verbose_name=_("Total dref allocation"), null=True, blank=True) + date_of_publication = models.DateField(verbose_name=_("Date of publication"), blank=True, null=True) + total_operation_timeframe = models.IntegerField(verbose_name=_("Total Operation Timeframe"), null=True, blank=True) + operation_start_date = models.DateField(verbose_name=_("Operation Start Date"), null=True, blank=True) + appeal_code = models.CharField(verbose_name=_("appeal code"), max_length=255, null=True, blank=True) + glide_code = models.CharField(verbose_name=_("glide number"), max_length=255, null=True, blank=True) ifrc_appeal_manager_name = models.CharField( - verbose_name=_('ifrc appeal manager name'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc appeal manager name"), max_length=255, null=True, blank=True ) ifrc_appeal_manager_email = models.CharField( - verbose_name=_('ifrc appeal manager email'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc appeal manager email"), max_length=255, null=True, blank=True ) ifrc_appeal_manager_title = models.CharField( - verbose_name=_('ifrc appeal manager title'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc appeal manager title"), max_length=255, null=True, blank=True ) ifrc_appeal_manager_phone_number = models.CharField( - verbose_name=_('ifrc appeal manager phone number'), max_length=100, - null=True, blank=True + verbose_name=_("ifrc appeal manager phone number"), max_length=100, null=True, blank=True ) ifrc_project_manager_name = models.CharField( - verbose_name=_('ifrc project manager name'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc project manager name"), max_length=255, null=True, blank=True ) ifrc_project_manager_email = models.CharField( - verbose_name=_('ifrc project manager email'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc project manager email"), max_length=255, null=True, blank=True ) ifrc_project_manager_title = models.CharField( - verbose_name=_('ifrc project manager title'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc project manager title"), max_length=255, null=True, blank=True ) ifrc_project_manager_phone_number = models.CharField( - verbose_name=_('ifrc project manager phone number'), max_length=100, - null=True, blank=True + verbose_name=_("ifrc project manager phone number"), max_length=100, null=True, blank=True ) national_society_contact_name = models.CharField( - verbose_name=_('national society contact name'), max_length=255, - null=True, blank=True + verbose_name=_("national society contact name"), max_length=255, null=True, blank=True ) national_society_contact_email = models.CharField( - verbose_name=_('national society contact email'), max_length=255, - null=True, blank=True + verbose_name=_("national society contact email"), max_length=255, null=True, blank=True ) national_society_contact_title = models.CharField( - verbose_name=_('national society contact title'), max_length=255, - null=True, blank=True + verbose_name=_("national society contact title"), max_length=255, null=True, blank=True ) national_society_contact_phone_number = models.CharField( - verbose_name=_('national society contact phone number'), max_length=100, - null=True, blank=True - ) - ifrc_emergency_name = models.CharField( - verbose_name=_('ifrc emergency name'), max_length=255, - null=True, blank=True - ) - ifrc_emergency_email = models.CharField( - verbose_name=_('ifrc emergency email'), max_length=255, - null=True, blank=True - ) - ifrc_emergency_title = models.CharField( - verbose_name=_('ifrc emergency title'), max_length=255, - null=True, blank=True + verbose_name=_("national society contact phone number"), max_length=100, null=True, blank=True ) + ifrc_emergency_name = models.CharField(verbose_name=_("ifrc emergency name"), max_length=255, null=True, blank=True) + ifrc_emergency_email = models.CharField(verbose_name=_("ifrc emergency email"), max_length=255, null=True, blank=True) + ifrc_emergency_title = models.CharField(verbose_name=_("ifrc emergency title"), max_length=255, null=True, blank=True) ifrc_emergency_phone_number = models.CharField( - verbose_name=_('ifrc emergency phone number'), max_length=100, - null=True, blank=True - ) - media_contact_name = models.CharField( - verbose_name=_('media contact name'), max_length=255, - null=True, blank=True - ) - media_contact_email = models.CharField( - verbose_name=_('media contact email'), max_length=255, - null=True, blank=True - ) - media_contact_title = models.CharField( - verbose_name=_('media contact title'), max_length=255, - null=True, blank=True + verbose_name=_("ifrc emergency phone number"), max_length=100, null=True, blank=True ) + media_contact_name = models.CharField(verbose_name=_("media contact name"), max_length=255, null=True, blank=True) + media_contact_email = models.CharField(verbose_name=_("media contact email"), max_length=255, null=True, blank=True) + media_contact_title = models.CharField(verbose_name=_("media contact title"), max_length=255, null=True, blank=True) media_contact_phone_number = models.CharField( - verbose_name=_('media_contact phone number'), max_length=100, - null=True, blank=True + verbose_name=_("media_contact phone number"), max_length=100, null=True, blank=True ) event_map = models.ForeignKey( - 'DrefFile', on_delete=models.SET_NULL, - null=True, blank=True, - verbose_name=_('event map'), - related_name='event_map_dref_final_report' + "DrefFile", + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name=_("event map"), + related_name="event_map_dref_final_report", ) photos = models.ManyToManyField( - 'DrefFile', blank=True, - verbose_name=_('images'), - related_name='photos_dref_final_report' + "DrefFile", blank=True, verbose_name=_("images"), related_name="photos_dref_final_report" ) assessment_report = models.ForeignKey( - 'DrefFile', on_delete=models.SET_NULL, - null=True, blank=True, - verbose_name=_('Assessment Report'), - related_name='dref_final_report_assessment_report' - ) - event_description = models.TextField( - verbose_name=_('Event description'), - null=True, blank=True - ) - anticipatory_actions = models.TextField( - verbose_name=_('Anticipatory actions'), - null=True, blank=True - ) - event_scope = models.TextField( - verbose_name=_('Event scope'), - null=True, blank=True - ) - ifrc = models.TextField( - verbose_name=_('ifrc'), - blank=True, null=True - ) - icrc = models.TextField( - verbose_name=_('icrc'), - blank=True, null=True - ) - partner_national_society = models.TextField( - verbose_name=_('partner national society'), - blank=True, null=True - ) + "DrefFile", + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name=_("Assessment Report"), + related_name="dref_final_report_assessment_report", + ) + event_description = models.TextField(verbose_name=_("Event description"), null=True, blank=True) + anticipatory_actions = models.TextField(verbose_name=_("Anticipatory actions"), null=True, blank=True) + event_scope = models.TextField(verbose_name=_("Event scope"), null=True, blank=True) + ifrc = models.TextField(verbose_name=_("ifrc"), blank=True, null=True) + icrc = models.TextField(verbose_name=_("icrc"), blank=True, null=True) + partner_national_society = models.TextField(verbose_name=_("partner national society"), blank=True, null=True) government_requested_assistance = models.BooleanField( - null=True, blank=True, - help_text=_('Has government requested assistance') - ) - national_authorities = models.TextField( - verbose_name=_('national authorities'), - blank=True, null=True - ) - un_or_other_actor = models.TextField( - verbose_name=_('un or other'), - blank=True, null=True + null=True, blank=True, help_text=_("Has government requested assistance") ) + national_authorities = models.TextField(verbose_name=_("national authorities"), blank=True, null=True) + un_or_other_actor = models.TextField(verbose_name=_("un or other"), blank=True, null=True) major_coordination_mechanism = models.TextField( - blank=True, null=True, - verbose_name=_('major coordination mechanism'), - ) - needs_identified = models.ManyToManyField( - IdentifiedNeed, verbose_name=_('needs identified'), - blank=True + blank=True, + null=True, + verbose_name=_("major coordination mechanism"), ) - people_assisted = models.TextField(verbose_name=_('people assisted'), blank=True, null=True) + needs_identified = models.ManyToManyField(IdentifiedNeed, verbose_name=_("needs identified"), blank=True) + people_assisted = models.TextField(verbose_name=_("people assisted"), blank=True, null=True) selection_criteria = models.TextField( - verbose_name=_('selection criteria'), blank=True, null=True, - help_text=_('Selection criteria for affected people') + verbose_name=_("selection criteria"), blank=True, null=True, help_text=_("Selection criteria for affected people") ) community_involved = models.TextField( - verbose_name=_('community involved'), blank=True, null=True, - help_text=_('Community been involved in the analysis of the process') + verbose_name=_("community involved"), + blank=True, + null=True, + help_text=_("Community been involved in the analysis of the process"), ) entity_affected = models.TextField( - verbose_name=_('entity affected'), blank=True, null=True, - help_text=_('Protection, gender, Inclusion affected in this process') + verbose_name=_("entity affected"), + blank=True, + null=True, + help_text=_("Protection, gender, Inclusion affected in this process"), ) change_in_operational_strategy = models.BooleanField( - verbose_name=_('Change in operational strategy'), + verbose_name=_("Change in operational strategy"), default=False, ) change_in_operational_strategy_text = models.TextField( - verbose_name=_('Change in operational strategy'), - null=True, blank=True - ) - women = models.IntegerField(verbose_name=_('women'), blank=True, null=True) - men = models.IntegerField(verbose_name=_('men'), blank=True, null=True) - girls = models.IntegerField( - verbose_name=_('girls'), help_text=_('Girls under 18'), - blank=True, null=True - ) - boys = models.IntegerField( - verbose_name=_('boys'), help_text=_('Boys under 18'), - blank=True, null=True + verbose_name=_("Change in operational strategy"), null=True, blank=True ) + women = models.IntegerField(verbose_name=_("women"), blank=True, null=True) + men = models.IntegerField(verbose_name=_("men"), blank=True, null=True) + girls = models.IntegerField(verbose_name=_("girls"), help_text=_("Girls under 18"), blank=True, null=True) + boys = models.IntegerField(verbose_name=_("boys"), help_text=_("Boys under 18"), blank=True, null=True) disability_people_per = models.DecimalField( - verbose_name=_('disability people per'), - blank=True, null=True, - max_digits=5, decimal_places=2 + verbose_name=_("disability people per"), blank=True, null=True, max_digits=5, decimal_places=2 ) people_per_urban = models.DecimalField( - verbose_name=_('people per urban'), - blank=True, null=True, - max_digits=5, decimal_places=2 + verbose_name=_("people per urban"), blank=True, null=True, max_digits=5, decimal_places=2 ) people_per_local = models.DecimalField( - verbose_name=_('people per local'), - blank=True, null=True, - max_digits=5, decimal_places=2 + verbose_name=_("people per local"), blank=True, null=True, max_digits=5, decimal_places=2 ) people_targeted_with_early_actions = models.IntegerField( - verbose_name=_('people targeted with early actions'), - blank=True, null=True - ) - displaced_people = models.IntegerField( - verbose_name=_('displaced people'), - blank=True, null=True + verbose_name=_("people targeted with early actions"), blank=True, null=True ) + displaced_people = models.IntegerField(verbose_name=_("displaced people"), blank=True, null=True) operation_objective = models.TextField( - verbose_name=_('operation objective'), - blank=True, null=True, + verbose_name=_("operation objective"), + blank=True, + null=True, ) response_strategy = models.TextField( - verbose_name=_('response strategy'), - blank=True, null=True, + verbose_name=_("response strategy"), + blank=True, + null=True, ) want_to_report = models.BooleanField( - verbose_name=_('Want to report'), + verbose_name=_("Want to report"), default=False, ) additional_national_society_actions = models.TextField( - verbose_name=_('Additional National Societies Actions'), - null=True, blank=True - ) - planned_interventions = models.ManyToManyField( - PlannedIntervention, - verbose_name=_('planned intervention'), - blank=True - ) - is_published = models.BooleanField( - verbose_name=_('Is Published'), - default=False + verbose_name=_("Additional National Societies Actions"), null=True, blank=True ) + planned_interventions = models.ManyToManyField(PlannedIntervention, verbose_name=_("planned intervention"), blank=True) + is_published = models.BooleanField(verbose_name=_("Is Published"), default=False) country = models.ForeignKey( - Country, verbose_name=_('country'), + Country, + verbose_name=_("country"), on_delete=models.CASCADE, - help_text=_('Affected County'), - null=True, blank=True, - related_name='final_report_country' + help_text=_("Affected County"), + null=True, + blank=True, + related_name="final_report_country", + ) + district = models.ManyToManyField(District, blank=True, verbose_name=_("district")) + users = models.ManyToManyField( + settings.AUTH_USER_MODEL, verbose_name=_("users"), blank=True, related_name="user_dref_final_report" + ) + is_assessment_report = models.BooleanField(verbose_name=_("Is assessment Report"), null=True, blank=True) + images = models.ManyToManyField("DrefFile", blank=True, verbose_name=_("images"), related_name="image_dref_final_report") + cover_image = models.ForeignKey( + "DrefFile", + on_delete=models.SET_NULL, + blank=True, + null=True, + verbose_name=_("cover image"), + related_name="cover_image_dref_final_report", + ) + is_there_major_coordination_mechanism = models.BooleanField( + null=True, blank=True, help_text=_("Is there major coordinate mechanism") ) - district = models.ManyToManyField( - District, blank=True, - verbose_name=_('district') + major_coordination_mechanism = models.TextField( + blank=True, + null=True, + verbose_name=_("major coordination mechanism"), + ) + total_targeted_population = models.IntegerField(verbose_name=_("total targeted population"), blank=True, null=True) + risk_security = models.ManyToManyField(RiskSecurity, blank=True, verbose_name=_("Risk Security")) + risk_security_concern = models.TextField(blank=True, null=True, verbose_name=_("Risk Security Concern")) + event_date = models.DateField( + verbose_name=_("event date"), null=True, blank=True, help_text=_("Date of event/Approximate date of impact") + ) + national_society_actions = models.ManyToManyField( + NationalSocietyAction, verbose_name=_("national society actions"), blank=True + ) + people_in_need = models.IntegerField(verbose_name=_("people in need"), blank=True, null=True) + event_text = models.TextField(verbose_name=_("event text"), blank=True, null=True) + ns_respond_date = models.DateField( + verbose_name=_("ns respond date"), null=True, blank=True, help_text=_("NS anticipatory actions started/NS response") ) + did_national_society = models.BooleanField(verbose_name=_("Did National Society"), null=True, blank=True) + financial_report = models.ForeignKey( + "DrefFile", + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name=_("financial report"), + related_name="financial_report_dref_final_report", + ) + num_assisted = models.IntegerField(verbose_name=_("number of assisted"), blank=True, null=True) + has_national_society_conducted = models.BooleanField( + verbose_name=_("Has national society conducted any intervention"), null=True, blank=True + ) + national_society_conducted_description = models.TextField( + verbose_name=_("National Society conducted description"), null=True, blank=True + ) + financial_report_description = models.TextField(verbose_name=_("Financial Report Description"), null=True, blank=True) class Meta: - verbose_name = _('Dref Final Report') - verbose_name_plural = _('Dref Final Reports') + verbose_name = _("Dref Final Report") + verbose_name_plural = _("Dref Final Reports") + + @staticmethod + def get_for(user): + from dref.utils import get_dref_users + + # get the user in dref + dref_users = get_dref_users() + current_user_dref = [] + for dref in dref_users: + for dref_user in dref["users"]: + if user.id == dref_user: + current_user_dref.append(dref) + current_user_dref_id = [] + for dref in current_user_dref: + current_user_dref_id.append(dref["id"]) + final_report_users = DrefFinalReport.objects.filter( + dref__in=current_user_dref_id, + ).distinct() + final_report_created_by = DrefFinalReport.objects.filter(created_by=user).distinct() + union_query = final_report_users.union(final_report_created_by) + return DrefFinalReport.objects.filter(id__in=union_query.values("id")).distinct() diff --git a/dref/permissions.py b/dref/permissions.py index ff87633e9..a96fb9d15 100644 --- a/dref/permissions.py +++ b/dref/permissions.py @@ -1,21 +1,34 @@ -from django.db import models - from rest_framework import permissions -from dref.models import Dref +from dref.models import DrefOperationalUpdate, DrefFinalReport +from dref.utils import get_dref_users + + +class DrefOperationalUpdateUpdatePermission(permissions.BasePermission): + message = "Can update Operational Update for whom dref is shared with" + + def has_object_permission(self, request, view, obj): + user = request.user + dref_objects = get_dref_users() + user_dref_ids = [] + for dref in dref_objects: + if user.id in dref.get("users"): + user_dref_ids.append(dref.get("id")) + for dref in user_dref_ids: + return DrefOperationalUpdate.objects.filter(dref=dref).exists() + return False -class DrefOperationalUpdateCreatePermission(permissions.BasePermission): - message = "Can create Operational Update for whom dref is shared with" +class DrefFinalReportUpdatePermission(permissions.BasePermission): + message = "Can update Final Report for whom dref is shared with" - def has_permission(self, request, view): - if request.method != "POST": - return True + def has_object_permission(self, request, view, obj): user = request.user - dref = request.data.get('dref') - if dref: - return Dref.objects.filter( - models.Q(id=dref, users=user) | - models.Q(id=dref, created_by=user) - ).exists() - return True + dref_objects = get_dref_users() + user_dref_ids = [] + for dref in dref_objects: + if user.id in dref.get("users"): + user_dref_ids.append(dref.get("id")) + for dref in user_dref_ids: + return DrefFinalReport.objects.filter(dref=dref).exists() + return False diff --git a/dref/search_indexes.py b/dref/search_indexes.py new file mode 100644 index 000000000..1bdfe6045 --- /dev/null +++ b/dref/search_indexes.py @@ -0,0 +1,33 @@ +from haystack import indexes + +from dref.models import Dref, DrefOperationalUpdate + + +class DrefIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.EdgeNgramField(document=True, use_template=True) + name = indexes.EdgeNgramField(model_attr='title') + created_at = indexes.DateTimeField(model_attr='created_at') + code = indexes.CharField(model_attr='appeal_code', null=True) + country_name = indexes.CharField(model_attr='national_society__name', null=True) + country_id = indexes.CharField(model_attr='national_society__id', null=True) + + def get_model(self): + return Dref + + def index_queryset(self, using=None): + return self.get_model().objects.all() + + +class DrefOperationalUpdateIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.EdgeNgramField(document=True, use_template=True) + name = indexes.EdgeNgramField(model_attr='title') + created_at = indexes.DateTimeField(model_attr='created_at') + code = indexes.CharField(model_attr='appeal_code', null=True) + country_name = indexes.CharField(model_attr='national_society__name', null=True) + country_id = indexes.CharField(model_attr='national_society__id', null=True) + + def get_model(self): + return DrefOperationalUpdate + + def index_queryset(self, using=None): + return self.get_model().objects.all() diff --git a/dref/serializers.py b/dref/serializers.py index 7d7d96a6b..107719918 100644 --- a/dref/serializers.py +++ b/dref/serializers.py @@ -3,20 +3,14 @@ from django.utils.translation import gettext from django.db import models, transaction +from django.conf import settings + from rest_framework import serializers from lang.serializers import ModelSerializer -from main.writable_nested_serializers import ( - NestedCreateMixin, - NestedUpdateMixin -) -from api.serializers import ( - UserNameSerializer, - DisasterTypeSerializer, - MiniDistrictSerializer, - MiniCountrySerializer -) +from main.writable_nested_serializers import NestedCreateMixin, NestedUpdateMixin +from api.serializers import UserNameSerializer, DisasterTypeSerializer, MiniDistrictSerializer, MiniCountrySerializer from dref.models import ( Dref, @@ -27,68 +21,69 @@ RiskSecurity, DrefFile, DrefOperationalUpdate, - DrefFinalReport + DrefFinalReport, ) from .tasks import send_dref_email +from dref.utils import get_dref_users class RiskSecuritySerializer(ModelSerializer): class Meta: model = RiskSecurity - fields = '__all__' + fields = "__all__" class PlannedInterventionIndicatorsSerializer(ModelSerializer): class Meta: model = PlannedInterventionIndicators - fields = '__all__' + fields = "__all__" class DrefFileSerializer(ModelSerializer): - created_by_details = UserNameSerializer(source='created_by', read_only=True) + created_by_details = UserNameSerializer(source="created_by", read_only=True) file = serializers.FileField(required=False) class Meta: model = DrefFile - fields = '__all__' - read_only_fields = ('created_by',) + fields = "__all__" + read_only_fields = ("created_by",) def create(self, validated_data): - validated_data['created_by'] = self.context['request'].user + validated_data["created_by"] = self.context["request"].user return super().create(validated_data) class MiniDrefSerializer(serializers.ModelSerializer): - type_of_onset_display = serializers.CharField(source='get_type_of_onset_display', read_only=True) - disaster_category_display = serializers.CharField(source='get_disaster_category_display', read_only=True) + type_of_onset_display = serializers.CharField(source="get_type_of_onset_display", read_only=True) + disaster_category_display = serializers.CharField(source="get_disaster_category_display", read_only=True) class Meta: model = Dref fields = [ - 'id', - 'title', - 'national_society', - 'disaster_type', - 'type_of_onset', - 'disaster_category', - 'disaster_category_display', - 'type_of_onset_display' + "id", + "title", + "national_society", + "disaster_type", + "type_of_onset", + "disaster_category", + "disaster_category_display", + "type_of_onset_display", ] class PlannedInterventionSerializer(ModelSerializer): - budget_file_details = DrefFileSerializer(source='budget_file', read_only=True) + budget_file_details = DrefFileSerializer(source="budget_file", read_only=True) image_url = serializers.SerializerMethodField() indicators = PlannedInterventionIndicatorsSerializer(many=True, required=False) - title_display = serializers.CharField(source='get_title_display', read_only=True) + title_display = serializers.CharField(source="get_title_display", read_only=True) class Meta: model = PlannedIntervention - fields = '__all__' + fields = "__all__" def create(self, validated_data): - indicators = validated_data.pop('indicators', []) + indicators = validated_data.pop("indicators", []) intervention = super().create(validated_data) for indicator in indicators: ind_object = PlannedInterventionIndicators.objects.create(**indicator) @@ -97,58 +92,58 @@ def create(self, validated_data): def update(self, instance, validated_data): # TODO: implement this - indicators = validated_data.pop('indicators', []) + indicators = validated_data.pop("indicators", []) intervention = super().update(instance, validated_data) return intervention def get_image_url(self, plannedintervention): title = plannedintervention.title - if title and self.context and 'request' in self.context: - request = self.context['request'] + if title and self.context and "request" in self.context: + request = self.context["request"] return PlannedIntervention.get_image_map(title, request) return None class NationalSocietyActionSerializer(ModelSerializer): image_url = serializers.SerializerMethodField() - title_display = serializers.CharField(source='get_title_display', read_only=True) + title_display = serializers.CharField(source="get_title_display", read_only=True) class Meta: model = NationalSocietyAction fields = ( - 'id', - 'title', - 'description', - 'image_url', - 'title_display', + "id", + "title", + "description", + "image_url", + "title_display", ) def get_image_url(self, nationalsocietyactions): title = nationalsocietyactions.title - if title and self.context and 'request' in self.context: - request = self.context['request'] + if title and self.context and "request" in self.context: + request = self.context["request"] return NationalSocietyAction.get_image_map(title, request) return None class IdentifiedNeedSerializer(ModelSerializer): image_url = serializers.SerializerMethodField() - title_display = serializers.CharField(source='get_title_display', read_only=True) + title_display = serializers.CharField(source="get_title_display", read_only=True) class Meta: model = IdentifiedNeed fields = ( - 'id', - 'title', - 'description', - 'image_url', - 'title_display', + "id", + "title", + "description", + "image_url", + "title_display", ) def get_image_url(self, identifiedneed): title = identifiedneed.title - if title and self.context and 'request' in self.context: - request = self.context['request'] + if title and self.context and "request" in self.context: + request = self.context["request"] return IdentifiedNeed.get_image_map(title, request) return None @@ -157,10 +152,10 @@ class MiniOperationalUpdateSerializer(serializers.ModelSerializer): class Meta: model = DrefOperationalUpdate fields = [ - 'id', - 'title', - 'is_published', - 'operational_update_number', + "id", + "title", + "is_published", + "operational_update_number", ] @@ -168,117 +163,118 @@ class MiniDrefFinalReportSerializer(serializers.ModelSerializer): class Meta: model = DrefFinalReport fields = [ - 'id', - 'title', - 'is_published', + "id", + "title", + "is_published", ] -class DrefSerializer( - NestedUpdateMixin, - NestedCreateMixin, - ModelSerializer -): +class DrefSerializer(NestedUpdateMixin, NestedCreateMixin, ModelSerializer): MAX_NUMBER_OF_IMAGES = 2 ALLOWED_BUDGET_FILE_EXTENSIONS = ["pdf"] ALLOWED_ASSESSMENT_REPORT_EXTENSIONS = ["pdf", "docx", "pptx"] MAX_OPERATION_TIMEFRAME = 30 ASSESSMENT_REPORT_MAX_OPERATION_TIMEFRAME = 2 - DREF_UPDATE_ERROR_MESSAGE = "OBSOLETE_PAYLOAD" national_society_actions = NationalSocietyActionSerializer(many=True, required=False) needs_identified = IdentifiedNeedSerializer(many=True, required=False) planned_interventions = PlannedInterventionSerializer(many=True, required=False) - type_of_onset_display = serializers.CharField(source='get_type_of_onset_display', read_only=True) - disaster_category_display = serializers.CharField(source='get_disaster_category_display', read_only=True) - status_display = serializers.CharField(source='get_status_display', read_only=True) - modified_by_details = UserNameSerializer(source='modified_by', read_only=True) - event_map_file = DrefFileSerializer(source='event_map', required=False, allow_null=True) - images_file = DrefFileSerializer(many=True, required=False, allow_null=True, source='images') + type_of_onset_display = serializers.CharField(source="get_type_of_onset_display", read_only=True) + disaster_category_display = serializers.CharField(source="get_disaster_category_display", read_only=True) + status_display = serializers.CharField(source="get_status_display", read_only=True) + modified_by_details = UserNameSerializer(source="modified_by", read_only=True) + event_map_file = DrefFileSerializer(source="event_map", required=False, allow_null=True) + images_file = DrefFileSerializer(many=True, required=False, allow_null=True, source="images") # field_report_details = MiniFieldReportSerializer(source='field_report', read_only=True) - created_by_details = UserNameSerializer(source='created_by', read_only=True) - users_details = UserNameSerializer(source='users', many=True, read_only=True) - budget_file_details = DrefFileSerializer(source='budget_file', read_only=True) - cover_image_file = DrefFileSerializer(source='cover_image', required=False, allow_null=True) - disaster_type_details = DisasterTypeSerializer(source='disaster_type', read_only=True) - operational_update_details = MiniOperationalUpdateSerializer(source='drefoperationalupdate_set', many=True, read_only=True) - dref_final_report_details = MiniDrefFinalReportSerializer(source='dreffinalreport', read_only=True) - country_details = MiniCountrySerializer(source='country', read_only=True) - district_details = MiniDistrictSerializer(source='district', read_only=True, many=True) - assessment_report_details = DrefFileSerializer(source='assessment_report', read_only=True) - supporting_document_details = DrefFileSerializer(read_only=True, source='supporting_document') + created_by_details = UserNameSerializer(source="created_by", read_only=True) + users_details = UserNameSerializer(source="users", many=True, read_only=True) + budget_file_details = DrefFileSerializer(source="budget_file", read_only=True) + cover_image_file = DrefFileSerializer(source="cover_image", required=False, allow_null=True) + disaster_type_details = DisasterTypeSerializer(source="disaster_type", read_only=True) + operational_update_details = MiniOperationalUpdateSerializer( + source="drefoperationalupdate_set", many=True, read_only=True + ) + dref_final_report_details = MiniDrefFinalReportSerializer(source="dreffinalreport", read_only=True) + country_details = MiniCountrySerializer(source="country", read_only=True) + district_details = MiniDistrictSerializer(source="district", read_only=True, many=True) + assessment_report_details = DrefFileSerializer(source="assessment_report", read_only=True) + supporting_document_details = DrefFileSerializer(read_only=True, source="supporting_document") risk_security = RiskSecuritySerializer(many=True, required=False) modified_at = serializers.DateTimeField(required=False) + dref_access_user_list = serializers.SerializerMethodField() class Meta: model = Dref - read_only_fields = ('modified_by', 'created_by', 'budget_file_preview') - exclude = ('cover_image', 'event_map', 'images') + read_only_fields = ("modified_by", "created_by", "budget_file_preview") + exclude = ("cover_image", "event_map", "images") + + def get_dref_access_user_list(self, obj): + dref_users_list = get_dref_users() + for dref in dref_users_list: + if obj.id == dref["id"]: + return dref["users"] + return def to_representation(self, instance): def _remove_digits_after_decimal(value): # NOTE: We are doing this to remove decimal after 3 digits whole numbers # eg: 100.00% be replaced with 100% - if value and len(value.split('.')[0]) == 3: - return value.split('.')[0] + if value and len(value.split(".")[0]) == 3: + return value.split(".")[0] return value data = super().to_representation(instance) for key in [ - 'disability_people_per', - 'people_per_urban', - 'people_per_local', + "disability_people_per", + "people_per_urban", + "people_per_local", ]: - value = data.get(key) or '' + value = data.get(key) or "" data[key] = _remove_digits_after_decimal(value) return data def validate(self, data): - event_date = data.get('event_date') - operation_timeframe = data.get('operation_timeframe') - is_assessment_report = data.get('is_assessment_report') - if event_date and data['type_of_onset'] not in [Dref.OnsetType.SLOW, Dref.OnsetType.SUDDEN]: - raise serializers.ValidationError({ - 'event_date': gettext('Cannot add event_date if onset type not in %s or %s' % (Dref.OnsetType.SLOW.label, Dref.OnsetType.SUDDEN.label)) - }) - if self.instance and self.instance.is_published: - raise serializers.ValidationError('Published Dref can\'t be changed. Please contact Admin') - if self.instance and DrefFinalReport.objects.filter(dref=self.instance, is_published=True).exists(): + event_date = data.get("event_date") + operation_timeframe = data.get("operation_timeframe") + is_assessment_report = data.get("is_assessment_report") + if event_date and data["type_of_onset"] not in [Dref.OnsetType.SLOW, Dref.OnsetType.SUDDEN]: raise serializers.ValidationError( - gettext('Can\'t Update %s dref for publish Field Report' % self.instance.id) + { + "event_date": gettext( + "Cannot add event_date if onset type not in %s or %s" + % (Dref.OnsetType.SLOW.label, Dref.OnsetType.SUDDEN.label) + ) + } ) + if self.instance and self.instance.is_published: + raise serializers.ValidationError("Published Dref can't be changed. Please contact Admin") + if self.instance and DrefFinalReport.objects.filter(dref=self.instance, is_published=True).exists(): + raise serializers.ValidationError(gettext("Can't Update %s dref for publish Field Report" % self.instance.id)) if operation_timeframe and is_assessment_report and operation_timeframe > 30: raise serializers.ValidationError( - gettext('Operation timeframe can\'t be greater than %s for assessment_report' % self.MAX_OPERATION_TIMEFRAME) - ) - if operation_timeframe and is_assessment_report and data['type_of_onset'] == Dref.OnsetType.SUDDEN and operation_timeframe > 2: - raise serializers.ValidationError( - gettext('Operation timeframe can\'t be greater than %s for assessment_report and Sudden Type' % self.ASSESSMENT_REPORT_MAX_OPERATION_TIMEFRAME) + gettext("Operation timeframe can't be greater than %s for assessment_report" % self.MAX_OPERATION_TIMEFRAME) ) return data def validate_images(self, images): # Don't allow images more than MAX_NUMBER_OF_IMAGES if len(images) > self.MAX_NUMBER_OF_IMAGES: - raise serializers.ValidationError( - gettext('Can add utmost %s images' % self.MAX_NUMBER_OF_IMAGES) - ) + raise serializers.ValidationError(gettext("Can add utmost %s images" % self.MAX_NUMBER_OF_IMAGES)) images_id = [image.id for image in images] images_without_access_qs = DrefFile.objects.filter( # Not created by current user - ~models.Q(created_by=self.context['request'].user), + ~models.Q(created_by=self.context["request"].user), # Look into provided images id__in=images_id, ) # Exclude already attached images if exists if self.instance: - images_without_access_qs = images_without_access_qs.exclude( - id__in=self.instance.images.all() - ) - images_id_without_access = images_without_access_qs.values_list('id', flat=True) + images_without_access_qs = images_without_access_qs.exclude(id__in=self.instance.images.all()) + images_id_without_access = images_without_access_qs.values_list("id", flat=True) if images_id_without_access: raise serializers.ValidationError( gettext( - 'Only image owner can attach image. Not allowed image ids: %s' % ','.join(map(str, images_id_without_access)) + "Only image owner can attach image. Not allowed image ids: %s" + % ",".join(map(str, images_id_without_access)) ) ) return images @@ -288,9 +284,7 @@ def validate_budget_file(self, budget_file): return extension = os.path.splitext(budget_file.file.name)[1].replace(".", "") if extension.lower() not in self.ALLOWED_BUDGET_FILE_EXTENSIONS: - raise serializers.ValidationError( - f'Invalid uploaded file extension: {extension}, Supported only PDF Files' - ) + raise serializers.ValidationError(f"Invalid uploaded file extension: {extension}, Supported only PDF Files") return budget_file def validate_assessment_report(self, assessment_report): @@ -299,144 +293,138 @@ def validate_assessment_report(self, assessment_report): extension = os.path.splitext(assessment_report.file.name)[1].replace(".", "") if extension.lower() not in self.ALLOWED_ASSESSMENT_REPORT_EXTENSIONS: raise serializers.ValidationError( - f'Invalid uploaded file extension: {extension}, Supported only {self.ALLOWED_ASSESSMENT_REPORT_EXTENSIONS} Files' + f"Invalid uploaded file extension: {extension}, Supported only {self.ALLOWED_ASSESSMENT_REPORT_EXTENSIONS} Files" ) return assessment_report def validate_operation_timeframe(self, operation_timeframe): if operation_timeframe and operation_timeframe > self.MAX_OPERATION_TIMEFRAME: raise serializers.ValidationError( - gettext(f'Operation timeframe can\'t be greater than {self.MAX_OPERATION_TIMEFRAME}') + gettext(f"Operation timeframe can't be greater than {self.MAX_OPERATION_TIMEFRAME}") ) return operation_timeframe def create(self, validated_data): - validated_data['created_by'] = self.context['request'].user - is_assessment_report = validated_data.get('is_assessment_report') + validated_data["created_by"] = self.context["request"].user + is_assessment_report = validated_data.get("is_assessment_report") if is_assessment_report: # Previous Operations - validated_data['lessons_learned'] = None - validated_data['affect_same_area'] = None - validated_data['affect_same_population'] = None - validated_data['ns_respond'] = None - validated_data['ns_request_fund'] = None - validated_data['assessment_report'] = None + validated_data["lessons_learned"] = None + validated_data["affect_same_area"] = None + validated_data["affect_same_population"] = None + validated_data["ns_respond"] = None + validated_data["ns_request_fund"] = None + validated_data["assessment_report"] = None # Event Description - validated_data['event_scope'] = None - validated_data['identified_gaps'] = None + validated_data["event_scope"] = None + validated_data["identified_gaps"] = None # Targeted Population - validated_data['women'] = None - validated_data['men'] = None - validated_data['girls'] = None - validated_data['boys'] = None + validated_data["women"] = None + validated_data["men"] = None + validated_data["girls"] = None + validated_data["boys"] = None # Support Services - validated_data['logistic_capacity_of_ns'] = None - validated_data['pmer'] = None - validated_data['communication'] = None + validated_data["logistic_capacity_of_ns"] = None + validated_data["pmer"] = None + validated_data["communication"] = None dref_assessment_report = super().create(validated_data) dref_assessment_report.needs_identified.clear() return dref_assessment_report - if 'users' in validated_data: - to = {u.email for u in validated_data['users']} - to.add('daniel.tovari@ifrc.org') # TODO remove me + if "users" in validated_data: + to = {u.email for u in validated_data["users"]} + to.add("daniel.tovari@ifrc.org") # TODO remove me else: to = None dref = super().create(validated_data) if to: - transaction.on_commit( - lambda: send_dref_email.delay(dref.id, list(to), 'New') - ) + transaction.on_commit(lambda: send_dref_email.delay(dref.id, list(to), "New")) return dref def update(self, instance, validated_data): - validated_data['modified_by'] = self.context['request'].user - is_assessment_report = validated_data.get('is_assessment_report') - modified_at = validated_data.pop('modified_at', None) + validated_data["modified_by"] = self.context["request"].user + is_assessment_report = validated_data.get("is_assessment_report") + modified_at = validated_data.pop("modified_at", None) if modified_at is None: - raise serializers.ValidationError({ 'modified_at': 'Modified At is required!' }) + raise serializers.ValidationError({"modified_at": "Modified At is required!"}) if is_assessment_report: # Previous Operations - validated_data['lessons_learned'] = None - validated_data['affect_same_area'] = None - validated_data['affect_same_population'] = None - validated_data['ns_respond'] = None - validated_data['ns_request_fund'] = None - validated_data['ns_request_text'] = None - validated_data['dref_recurrent_text'] = None - validated_data['assessment_report'] = None + validated_data["lessons_learned"] = None + validated_data["affect_same_area"] = None + validated_data["affect_same_population"] = None + validated_data["ns_respond"] = None + validated_data["ns_request_fund"] = None + validated_data["ns_request_text"] = None + validated_data["dref_recurrent_text"] = None + validated_data["assessment_report"] = None # Event Description - validated_data['event_scope'] = None - validated_data['identified_gaps'] = None + validated_data["event_scope"] = None + validated_data["identified_gaps"] = None # Targeted Population - validated_data['women'] = None - validated_data['men'] = None - validated_data['girls'] = None - validated_data['boys'] = None + validated_data["women"] = None + validated_data["men"] = None + validated_data["girls"] = None + validated_data["boys"] = None # Support Services - validated_data['logistic_capacity_of_ns'] = None - validated_data['pmer'] = None - validated_data['communication'] = None + validated_data["logistic_capacity_of_ns"] = None + validated_data["pmer"] = None + validated_data["communication"] = None dref_assessment_report = super().update(instance, validated_data) dref_assessment_report.needs_identified.clear() return dref_assessment_report # we don't send notification again to the already notified users: - if 'users' in validated_data: - to = {u.email for u in validated_data['users'] - if u.email not in {t.email for t in instance.users.iterator()}} + if "users" in validated_data: + to = {u.email for u in validated_data["users"] if u.email not in {t.email for t in instance.users.iterator()}} else: to = None if modified_at and instance.modified_at and modified_at < instance.modified_at: - raise serializers.ValidationError({ 'modified_at': self.DREF_UPDATE_ERROR_MESSAGE }) + raise serializers.ValidationError({"modified_at": settings.DREF_OP_UPDATE_FINAL_REPORT_UPDATE_ERROR_MESSAGE}) dref = super().update(instance, validated_data) if to: - transaction.on_commit( - lambda: send_dref_email.delay(dref.id, list(to), 'Updated') - ) + transaction.on_commit(lambda: send_dref_email.delay(dref.id, list(to), "Updated")) return dref -class DrefOperationalUpdateSerializer( - NestedUpdateMixin, - NestedCreateMixin, - serializers.ModelSerializer -): +class DrefOperationalUpdateSerializer(NestedUpdateMixin, NestedCreateMixin, serializers.ModelSerializer): national_society_actions = NationalSocietyActionSerializer(many=True, required=False) needs_identified = IdentifiedNeedSerializer(many=True, required=False) planned_interventions = PlannedInterventionSerializer(many=True, required=False) - type_of_onset_display = serializers.CharField(source='get_type_of_onset_display', read_only=True) - disaster_category_display = serializers.CharField(source='get_disaster_category_display', read_only=True) - created_by_details = UserNameSerializer(source='created_by', read_only=True) - event_map_file = DrefFileSerializer(source='event_map', required=False, allow_null=True) - cover_image_file = DrefFileSerializer(source='cover_image', required=False, allow_null=True) - images_file = DrefFileSerializer(many=True, required=False, allow_null=True, source='images') - photos_file = DrefFileSerializer(source='photos', many=True, required=False, allow_null=True) - modified_by_details = UserNameSerializer(source='modified_by', read_only=True) - disaster_type_details = DisasterTypeSerializer(source='disaster_type', read_only=True) - budget_file_details = DrefFileSerializer(source='budget_file', read_only=True) - country_details = MiniCountrySerializer(source='country', read_only=True) - district_details = MiniDistrictSerializer(source='district', read_only=True, many=True) - assessment_report_file = DrefFileSerializer(source='assessment_report', required=False, allow_null=True) + type_of_onset_display = serializers.CharField(source="get_type_of_onset_display", read_only=True) + disaster_category_display = serializers.CharField(source="get_disaster_category_display", read_only=True) + created_by_details = UserNameSerializer(source="created_by", read_only=True) + event_map_file = DrefFileSerializer(source="event_map", required=False, allow_null=True) + cover_image_file = DrefFileSerializer(source="cover_image", required=False, allow_null=True) + images_file = DrefFileSerializer(many=True, required=False, allow_null=True, source="images") + photos_file = DrefFileSerializer(source="photos", many=True, required=False, allow_null=True) + modified_by_details = UserNameSerializer(source="modified_by", read_only=True) + disaster_type_details = DisasterTypeSerializer(source="disaster_type", read_only=True) + budget_file_details = DrefFileSerializer(source="budget_file", read_only=True) + country_details = MiniCountrySerializer(source="country", read_only=True) + district_details = MiniDistrictSerializer(source="district", read_only=True, many=True) + assessment_report_details = DrefFileSerializer(source="assessment_report", read_only=True) risk_security = RiskSecuritySerializer(many=True, required=False) + modified_at = serializers.DateTimeField(required=False) class Meta: model = DrefOperationalUpdate - read_only_fields = ('operational_update_number', ) - exclude = ('images', 'photos', 'event_map', 'assessment_report', 'cover_image') + read_only_fields = ("operational_update_number",) + exclude = ("images", "photos", "event_map", "cover_image") def validate(self, data): - dref = data.get('dref') + dref = data.get("dref") if not self.instance and dref: if not dref.is_published: raise serializers.ValidationError( - gettext('Can\'t create Operational Update for not published %s dref.' % dref.id) + gettext("Can't create Operational Update for not published %s dref." % dref.id) ) # get the latest dref_operation_update and check whether it is published or not, exclude no operational object created so far - dref_operational_update = DrefOperationalUpdate.objects.filter(dref=dref).order_by('-operational_update_number').first() + dref_operational_update = ( + DrefOperationalUpdate.objects.filter(dref=dref).order_by("-operational_update_number").first() + ) if dref_operational_update and not dref_operational_update.is_published: raise serializers.ValidationError( gettext( - 'Can\'t create Operational Update for not published Operational Update %s id and Operational Update Number %i.' + "Can't create Operational Update for not published Operational Update %s id and Operational Update Number %i." % (dref_operational_update.id, dref_operational_update.operational_update_number) ) ) @@ -445,107 +433,110 @@ def validate(self, data): def validate_appeal_code(self, appeal_code): if appeal_code != self.instance.appeal_code: - raise serializers.ValidationError('Can\'t edit MDR Code') + raise serializers.ValidationError("Can't edit MDR Code") return appeal_code def get_total_timeframe(self, start_date, end_date): if start_date and end_date: - start_date_month = datetime.datetime.strftime('%m') - end_date_month = datetime.datetime.strptime('%m') + start_date_month = datetime.datetime.strftime("%m") + end_date_month = datetime.datetime.strptime("%m") return abs(end_date_month - start_date_month) return None def create(self, validated_data): - dref = validated_data['dref'] - dref_operational_update = DrefOperationalUpdate.objects.filter(dref=dref).order_by('-operational_update_number').first() + dref = validated_data["dref"] + dref_operational_update = ( + DrefOperationalUpdate.objects.filter(dref=dref).order_by("-operational_update_number").first() + ) if not dref_operational_update: - validated_data['title'] = dref.title - validated_data['title_prefix'] = dref.title_prefix - validated_data['national_society'] = dref.national_society - validated_data['disaster_type'] = dref.disaster_type - validated_data['type_of_onset'] = dref.type_of_onset - validated_data['disaster_category'] = dref.disaster_category - validated_data['number_of_people_targeted'] = dref.num_assisted - validated_data['number_of_people_affected'] = dref.num_affected - validated_data['emergency_appeal_planned'] = dref.emergency_appeal_planned - validated_data['appeal_code'] = dref.appeal_code - validated_data['glide_code'] = dref.glide_code - validated_data['ifrc_appeal_manager_name'] = dref.ifrc_appeal_manager_name - validated_data['ifrc_appeal_manager_email'] = dref.ifrc_appeal_manager_email - validated_data['ifrc_appeal_manager_title'] = dref.ifrc_appeal_manager_title - validated_data['ifrc_appeal_manager_phone_number'] = dref.ifrc_appeal_manager_phone_number - validated_data['ifrc_project_manager_name'] = dref.ifrc_project_manager_name - validated_data['ifrc_project_manager_email'] = dref.ifrc_project_manager_email - validated_data['ifrc_project_manager_title'] = dref.ifrc_project_manager_title - validated_data['ifrc_project_manager_phone_number'] = dref.ifrc_project_manager_phone_number - validated_data['national_society_contact_name'] = dref.national_society_contact_name - validated_data['national_society_contact_email'] = dref.national_society_contact_email - validated_data['national_society_contact_title'] = dref.national_society_contact_title - validated_data['national_society_contact_phone_number'] = dref.national_society_contact_phone_number - validated_data['media_contact_name'] = dref.media_contact_name - validated_data['media_contact_email'] = dref.media_contact_email - validated_data['media_contact_title'] = dref.media_contact_title - validated_data['media_contact_phone_number'] = dref.media_contact_phone_number - validated_data['ifrc_emergency_name'] = dref.ifrc_emergency_name - validated_data['ifrc_emergency_title'] = dref.ifrc_emergency_title - validated_data['ifrc_emergency_phone_number'] = dref.ifrc_emergency_phone_number - validated_data['ifrc_emergency_email'] = dref.ifrc_emergency_email - validated_data['ifrc'] = dref.ifrc - validated_data['icrc'] = dref.icrc - validated_data['partner_national_society'] = dref.partner_national_society - validated_data['government_requested_assistance'] = dref.government_requested_assistance - validated_data['national_authorities'] = dref.national_authorities - validated_data['un_or_other_actor'] = dref.un_or_other_actor - validated_data['major_coordination_mechanism'] = dref.major_coordination_mechanism - validated_data['people_assisted'] = dref.people_assisted - validated_data['selection_criteria'] = dref.selection_criteria - validated_data['entity_affected'] = dref.entity_affected - validated_data['women'] = dref.women - validated_data['men'] = dref.men - validated_data['girls'] = dref.girls - validated_data['boys'] = dref.boys - validated_data['disability_people_per'] = dref.disability_people_per - validated_data['people_per_urban'] = dref.people_per_urban - validated_data['people_per_local'] = dref.people_per_local - validated_data['people_targeted_with_early_actions'] = dref.people_targeted_with_early_actions - validated_data['displaced_people'] = dref.displaced_people - validated_data['operation_objective'] = dref.operation_objective - validated_data['response_strategy'] = dref.response_strategy - validated_data['created_by'] = self.context['request'].user - validated_data['new_operational_start_date'] = dref.date_of_approval - validated_data['operational_update_number'] = 1 # if no any dref operational update created so far - validated_data['dref_allocated_so_far'] = dref.amount_requested - validated_data['event_description'] = dref.event_description - validated_data['anticipatory_actions'] = dref.anticipatory_actions - validated_data['event_scope'] = dref.event_scope - validated_data['budget_file'] = dref.budget_file - validated_data['country'] = dref.country - validated_data['risk_security_concern'] = dref.risk_security_concern - validated_data['is_assessment_report'] = dref.is_assessment_report - validated_data['event_date'] = dref.event_date - validated_data['ns_respond_date'] = dref.ns_respond_date - validated_data['ns_respond'] = dref.ns_respond - validated_data['total_targeted_population'] = dref.total_targeted_population - validated_data['is_there_major_coordination_mechanism'] = dref.is_there_major_coordination_mechanism - validated_data['human_resource'] = dref.human_resource - validated_data['is_surge_personnel_deployed'] = dref.is_surge_personnel_deployed - validated_data['surge_personnel_deployed'] = dref.surge_personnel_deployed - validated_data['logistic_capacity_of_ns'] = dref.logistic_capacity_of_ns - validated_data['safety_concerns'] = dref.safety_concerns - validated_data['pmer'] = dref.pmer - validated_data['people_in_need'] = dref.people_in_need - validated_data['communication'] = dref.communication + validated_data["title"] = dref.title + validated_data["title_prefix"] = dref.title_prefix + validated_data["national_society"] = dref.national_society + validated_data["disaster_type"] = dref.disaster_type + validated_data["type_of_onset"] = dref.type_of_onset + validated_data["disaster_category"] = dref.disaster_category + validated_data["number_of_people_targeted"] = dref.num_assisted + validated_data["number_of_people_affected"] = dref.num_affected + validated_data["emergency_appeal_planned"] = dref.emergency_appeal_planned + validated_data["appeal_code"] = dref.appeal_code + validated_data["glide_code"] = dref.glide_code + validated_data["ifrc_appeal_manager_name"] = dref.ifrc_appeal_manager_name + validated_data["ifrc_appeal_manager_email"] = dref.ifrc_appeal_manager_email + validated_data["ifrc_appeal_manager_title"] = dref.ifrc_appeal_manager_title + validated_data["ifrc_appeal_manager_phone_number"] = dref.ifrc_appeal_manager_phone_number + validated_data["ifrc_project_manager_name"] = dref.ifrc_project_manager_name + validated_data["ifrc_project_manager_email"] = dref.ifrc_project_manager_email + validated_data["ifrc_project_manager_title"] = dref.ifrc_project_manager_title + validated_data["ifrc_project_manager_phone_number"] = dref.ifrc_project_manager_phone_number + validated_data["national_society_contact_name"] = dref.national_society_contact_name + validated_data["national_society_contact_email"] = dref.national_society_contact_email + validated_data["national_society_contact_title"] = dref.national_society_contact_title + validated_data["national_society_contact_phone_number"] = dref.national_society_contact_phone_number + validated_data["media_contact_name"] = dref.media_contact_name + validated_data["media_contact_email"] = dref.media_contact_email + validated_data["media_contact_title"] = dref.media_contact_title + validated_data["media_contact_phone_number"] = dref.media_contact_phone_number + validated_data["ifrc_emergency_name"] = dref.ifrc_emergency_name + validated_data["ifrc_emergency_title"] = dref.ifrc_emergency_title + validated_data["ifrc_emergency_phone_number"] = dref.ifrc_emergency_phone_number + validated_data["ifrc_emergency_email"] = dref.ifrc_emergency_email + validated_data["ifrc"] = dref.ifrc + validated_data["icrc"] = dref.icrc + validated_data["partner_national_society"] = dref.partner_national_society + validated_data["government_requested_assistance"] = dref.government_requested_assistance + validated_data["national_authorities"] = dref.national_authorities + validated_data["un_or_other_actor"] = dref.un_or_other_actor + validated_data["major_coordination_mechanism"] = dref.major_coordination_mechanism + validated_data["people_assisted"] = dref.people_assisted + validated_data["selection_criteria"] = dref.selection_criteria + validated_data["entity_affected"] = dref.entity_affected + validated_data["women"] = dref.women + validated_data["men"] = dref.men + validated_data["girls"] = dref.girls + validated_data["boys"] = dref.boys + validated_data["disability_people_per"] = dref.disability_people_per + validated_data["people_per_urban"] = dref.people_per_urban + validated_data["people_per_local"] = dref.people_per_local + validated_data["people_targeted_with_early_actions"] = dref.people_targeted_with_early_actions + validated_data["displaced_people"] = dref.displaced_people + validated_data["operation_objective"] = dref.operation_objective + validated_data["response_strategy"] = dref.response_strategy + validated_data["created_by"] = self.context["request"].user + validated_data["new_operational_start_date"] = dref.date_of_approval + validated_data["operational_update_number"] = 1 # if no any dref operational update created so far + validated_data["dref_allocated_so_far"] = dref.amount_requested + validated_data["event_description"] = dref.event_description + validated_data["anticipatory_actions"] = dref.anticipatory_actions + validated_data["event_scope"] = dref.event_scope + validated_data["budget_file"] = dref.budget_file + validated_data["assessment_report"] = dref.assessment_report + validated_data["country"] = dref.country + validated_data["risk_security_concern"] = dref.risk_security_concern + validated_data["is_assessment_report"] = dref.is_assessment_report + validated_data["event_date"] = dref.event_date + validated_data["ns_respond_date"] = dref.ns_respond_date + validated_data["ns_respond"] = dref.ns_respond + validated_data["total_targeted_population"] = dref.total_targeted_population + validated_data["is_there_major_coordination_mechanism"] = dref.is_there_major_coordination_mechanism + validated_data["human_resource"] = dref.human_resource + validated_data["is_surge_personnel_deployed"] = dref.is_surge_personnel_deployed + validated_data["surge_personnel_deployed"] = dref.surge_personnel_deployed + validated_data["logistic_capacity_of_ns"] = dref.logistic_capacity_of_ns + validated_data["safety_concerns"] = dref.safety_concerns + validated_data["pmer"] = dref.pmer + validated_data["people_in_need"] = dref.people_in_need + validated_data["communication"] = dref.communication + validated_data["total_operation_timeframe"] = dref.operation_timeframe operational_update = super().create(validated_data) # XXX: Copy files from DREF (Only nested serialized fields) nested_serialized_file_fields = [ - 'assessment_report', - 'cover_image', - 'event_map', + "cover_image", + "event_map", ] for file_field in nested_serialized_file_fields: dref_file = getattr(dref, file_field, None) if dref_file: - setattr(operational_update, file_field, dref_file.clone(self.context['request'].user)) + setattr(operational_update, file_field, dref_file.clone(self.context["request"].user)) operational_update.save(update_fields=nested_serialized_file_fields) # M2M Fields operational_update.planned_interventions.add(*dref.planned_interventions.all()) @@ -557,85 +548,95 @@ def create(self, validated_data): operational_update.risk_security.add(*dref.risk_security.all()) else: # get the latest dref operational update - validated_data['title'] = dref_operational_update.title - validated_data['title_prefix'] = dref_operational_update.title_prefix - validated_data['national_society'] = dref_operational_update.national_society - validated_data['disaster_type'] = dref_operational_update.disaster_type - validated_data['type_of_onset'] = dref_operational_update.type_of_onset - validated_data['disaster_category'] = dref_operational_update.disaster_category - validated_data['number_of_people_targeted'] = dref_operational_update.number_of_people_targeted - validated_data['number_of_people_affected'] = dref_operational_update.number_of_people_affected - validated_data['emergency_appeal_planned'] = dref_operational_update.emergency_appeal_planned - validated_data['appeal_code'] = dref_operational_update.appeal_code - validated_data['glide_code'] = dref_operational_update.glide_code - validated_data['ifrc_appeal_manager_name'] = dref_operational_update.ifrc_appeal_manager_name - validated_data['ifrc_appeal_manager_email'] = dref_operational_update.ifrc_appeal_manager_email - validated_data['ifrc_appeal_manager_title'] = dref_operational_update.ifrc_appeal_manager_title - validated_data['ifrc_appeal_manager_phone_number'] = dref_operational_update.ifrc_appeal_manager_phone_number - validated_data['ifrc_project_manager_name'] = dref_operational_update.ifrc_project_manager_name - validated_data['ifrc_project_manager_email'] = dref_operational_update.ifrc_project_manager_email - validated_data['ifrc_project_manager_title'] = dref_operational_update.ifrc_project_manager_title - validated_data['ifrc_project_manager_phone_number'] = dref_operational_update.ifrc_project_manager_phone_number - validated_data['national_society_contact_name'] = dref_operational_update.national_society_contact_name - validated_data['national_society_contact_email'] = dref_operational_update.national_society_contact_email - validated_data['national_society_contact_title'] = dref_operational_update.national_society_contact_title - validated_data['national_society_contact_phone_number'] = dref_operational_update.national_society_contact_phone_number - validated_data['media_contact_name'] = dref_operational_update.media_contact_name - validated_data['media_contact_email'] = dref_operational_update.media_contact_email - validated_data['media_contact_title'] = dref_operational_update.media_contact_title - validated_data['media_contact_phone_number'] = dref_operational_update.media_contact_phone_number - validated_data['ifrc_emergency_name'] = dref_operational_update.ifrc_emergency_name - validated_data['ifrc_emergency_title'] = dref_operational_update.ifrc_emergency_title - validated_data['ifrc_emergency_phone_number'] = dref_operational_update.ifrc_emergency_phone_number - validated_data['ifrc_emergency_email'] = dref_operational_update.ifrc_emergency_email - validated_data['ifrc'] = dref_operational_update.ifrc - validated_data['icrc'] = dref_operational_update.icrc - validated_data['partner_national_society'] = dref_operational_update.partner_national_society - validated_data['government_requested_assistance'] = dref_operational_update.government_requested_assistance - validated_data['national_authorities'] = dref_operational_update.national_authorities - validated_data['un_or_other_actor'] = dref_operational_update.un_or_other_actor - validated_data['major_coordination_mechanism'] = dref_operational_update.major_coordination_mechanism - validated_data['people_assisted'] = dref_operational_update.people_assisted - validated_data['selection_criteria'] = dref_operational_update.selection_criteria - validated_data['entity_affected'] = dref_operational_update.entity_affected - validated_data['women'] = dref_operational_update.women - validated_data['men'] = dref_operational_update.men - validated_data['girls'] = dref_operational_update.girls - validated_data['boys'] = dref_operational_update.boys - validated_data['disability_people_per'] = dref_operational_update.disability_people_per - validated_data['people_per_urban'] = dref_operational_update.people_per_urban - validated_data['people_per_local'] = dref_operational_update.people_per_local - validated_data['people_targeted_with_early_actions'] = dref_operational_update.people_targeted_with_early_actions - validated_data['displaced_people'] = dref_operational_update.displaced_people - validated_data['operation_objective'] = dref_operational_update.operation_objective - validated_data['response_strategy'] = dref_operational_update.response_strategy - validated_data['created_by'] = self.context['request'].user - validated_data['operational_update_number'] = dref_operational_update.operational_update_number + 1 - validated_data['new_operational_start_date'] = dref_operational_update.dref.date_of_approval - validated_data['dref_allocated_so_far'] = dref_operational_update.total_dref_allocation - validated_data['event_description'] = dref_operational_update.event_description - validated_data['anticipatory_actions'] = dref_operational_update.anticipatory_actions - validated_data['event_scope'] = dref_operational_update.event_scope - validated_data['cover_image'] = dref_operational_update.cover_image - validated_data['budget_file'] = dref_operational_update.budget_file - validated_data['assessment_report'] = dref_operational_update.assessment_report - validated_data['country'] = dref_operational_update.country - validated_data['risk_security_concern'] = dref_operational_update.risk_security_concern - validated_data['is_assessment_report'] = dref_operational_update.is_assessment_report - validated_data['event_date'] = dref_operational_update.event_date - validated_data['ns_respond_date'] = dref_operational_update.ns_respond_date - validated_data['ns_respond'] = dref_operational_update.ns_respond - validated_data['total_targeted_population'] = dref_operational_update.total_targeted_population - validated_data['is_there_major_coordination_mechanism'] = dref_operational_update.is_there_major_coordination_mechanism - validated_data['human_resource'] = dref_operational_update.human_resource - validated_data['is_surge_personnel_deployed'] = dref_operational_update.is_surge_personnel_deployed - validated_data['surge_personnel_deployed'] = dref_operational_update.surge_personnel_deployed - validated_data['logistic_capacity_of_ns'] = dref_operational_update.logistic_capacity_of_ns - validated_data['safety_concerns'] = dref_operational_update.safety_concerns - validated_data['pmer'] = dref_operational_update.pmer - validated_data['communication'] = dref_operational_update.communication - validated_data['people_in_need'] = dref_operational_update.people_in_need + validated_data["title"] = dref_operational_update.title + validated_data["title_prefix"] = dref_operational_update.title_prefix + validated_data["national_society"] = dref_operational_update.national_society + validated_data["disaster_type"] = dref_operational_update.disaster_type + validated_data["type_of_onset"] = dref_operational_update.type_of_onset + validated_data["disaster_category"] = dref_operational_update.disaster_category + validated_data["number_of_people_targeted"] = dref_operational_update.number_of_people_targeted + validated_data["number_of_people_affected"] = dref_operational_update.number_of_people_affected + validated_data["emergency_appeal_planned"] = dref_operational_update.emergency_appeal_planned + validated_data["appeal_code"] = dref_operational_update.appeal_code + validated_data["glide_code"] = dref_operational_update.glide_code + validated_data["ifrc_appeal_manager_name"] = dref_operational_update.ifrc_appeal_manager_name + validated_data["ifrc_appeal_manager_email"] = dref_operational_update.ifrc_appeal_manager_email + validated_data["ifrc_appeal_manager_title"] = dref_operational_update.ifrc_appeal_manager_title + validated_data["ifrc_appeal_manager_phone_number"] = dref_operational_update.ifrc_appeal_manager_phone_number + validated_data["ifrc_project_manager_name"] = dref_operational_update.ifrc_project_manager_name + validated_data["ifrc_project_manager_email"] = dref_operational_update.ifrc_project_manager_email + validated_data["ifrc_project_manager_title"] = dref_operational_update.ifrc_project_manager_title + validated_data["ifrc_project_manager_phone_number"] = dref_operational_update.ifrc_project_manager_phone_number + validated_data["national_society_contact_name"] = dref_operational_update.national_society_contact_name + validated_data["national_society_contact_email"] = dref_operational_update.national_society_contact_email + validated_data["national_society_contact_title"] = dref_operational_update.national_society_contact_title + validated_data["national_society_contact_phone_number"] = dref_operational_update.national_society_contact_phone_number + validated_data["media_contact_name"] = dref_operational_update.media_contact_name + validated_data["media_contact_email"] = dref_operational_update.media_contact_email + validated_data["media_contact_title"] = dref_operational_update.media_contact_title + validated_data["media_contact_phone_number"] = dref_operational_update.media_contact_phone_number + validated_data["ifrc_emergency_name"] = dref_operational_update.ifrc_emergency_name + validated_data["ifrc_emergency_title"] = dref_operational_update.ifrc_emergency_title + validated_data["ifrc_emergency_phone_number"] = dref_operational_update.ifrc_emergency_phone_number + validated_data["ifrc_emergency_email"] = dref_operational_update.ifrc_emergency_email + validated_data["ifrc"] = dref_operational_update.ifrc + validated_data["icrc"] = dref_operational_update.icrc + validated_data["partner_national_society"] = dref_operational_update.partner_national_society + validated_data["government_requested_assistance"] = dref_operational_update.government_requested_assistance + validated_data["national_authorities"] = dref_operational_update.national_authorities + validated_data["un_or_other_actor"] = dref_operational_update.un_or_other_actor + validated_data["major_coordination_mechanism"] = dref_operational_update.major_coordination_mechanism + validated_data["people_assisted"] = dref_operational_update.people_assisted + validated_data["selection_criteria"] = dref_operational_update.selection_criteria + validated_data["entity_affected"] = dref_operational_update.entity_affected + validated_data["women"] = dref_operational_update.women + validated_data["men"] = dref_operational_update.men + validated_data["girls"] = dref_operational_update.girls + validated_data["boys"] = dref_operational_update.boys + validated_data["disability_people_per"] = dref_operational_update.disability_people_per + validated_data["people_per_urban"] = dref_operational_update.people_per_urban + validated_data["people_per_local"] = dref_operational_update.people_per_local + validated_data["people_targeted_with_early_actions"] = dref_operational_update.people_targeted_with_early_actions + validated_data["displaced_people"] = dref_operational_update.displaced_people + validated_data["operation_objective"] = dref_operational_update.operation_objective + validated_data["response_strategy"] = dref_operational_update.response_strategy + validated_data["created_by"] = self.context["request"].user + validated_data["operational_update_number"] = dref_operational_update.operational_update_number + 1 + validated_data["new_operational_start_date"] = dref_operational_update.dref.date_of_approval + validated_data["dref_allocated_so_far"] = dref_operational_update.total_dref_allocation + validated_data["event_description"] = dref_operational_update.event_description + validated_data["anticipatory_actions"] = dref_operational_update.anticipatory_actions + validated_data["event_scope"] = dref_operational_update.event_scope + validated_data["budget_file"] = dref_operational_update.budget_file + validated_data["assessment_report"] = dref_operational_update.assessment_report + validated_data["country"] = dref_operational_update.country + validated_data["risk_security_concern"] = dref_operational_update.risk_security_concern + validated_data["is_assessment_report"] = dref_operational_update.is_assessment_report + validated_data["event_date"] = dref_operational_update.event_date + validated_data["ns_respond_date"] = dref_operational_update.ns_respond_date + validated_data["ns_respond"] = dref_operational_update.ns_respond + validated_data["total_targeted_population"] = dref_operational_update.total_targeted_population + validated_data["is_there_major_coordination_mechanism"] = dref_operational_update.is_there_major_coordination_mechanism + validated_data["human_resource"] = dref_operational_update.human_resource + validated_data["is_surge_personnel_deployed"] = dref_operational_update.is_surge_personnel_deployed + validated_data["surge_personnel_deployed"] = dref_operational_update.surge_personnel_deployed + validated_data["logistic_capacity_of_ns"] = dref_operational_update.logistic_capacity_of_ns + validated_data["safety_concerns"] = dref_operational_update.safety_concerns + validated_data["pmer"] = dref_operational_update.pmer + validated_data["communication"] = dref_operational_update.communication + validated_data["people_in_need"] = dref_operational_update.people_in_need + validated_data["total_operation_timeframe"] = dref_operational_update.total_operation_timeframe operational_update = super().create(validated_data) + # XXX: Copy files from DREF (Only nested serialized fields) + nested_serialized_file_fields = [ + "cover_image", + "event_map", + ] + for file_field in nested_serialized_file_fields: + dref_file = getattr(dref, file_field, None) + if dref_file: + setattr(operational_update, file_field, dref_file.clone(self.context["request"].user)) + operational_update.save(update_fields=nested_serialized_file_fields) # XXX COPY M2M fields operational_update.planned_interventions.add(*dref_operational_update.planned_interventions.all()) operational_update.images.add(*dref_operational_update.images.all()) @@ -647,246 +648,343 @@ def create(self, validated_data): return operational_update def update(self, instance, validated_data): - validated_data['updated_by'] = self.context['request'].user - changing_timeframe_operation = validated_data.get('changing_timeframe_operation', instance.changing_timeframe_operation) - total_operation_timeframe = validated_data.get('total_operation_timeframe', instance.total_operation_timeframe) - number_of_people_targeted = validated_data.get('number_of_people_targeted', instance.number_of_people_targeted) - request_for_second_allocation = validated_data.get('request_for_second_allocation', instance.request_for_second_allocation) - additional_allocation = validated_data.get('additional_allocation', instance.additional_allocation) - changing_target_population_of_operation = validated_data.get('changing_target_population_of_operation', instance.changing_target_population_of_operation) - changing_geographic_location = validated_data.get('changing_geographic_location', instance.changing_geographic_location) - district = validated_data.get('district', instance.district) - dref_operation_timeframe = validated_data.get('dref', instance.dref).operation_timeframe - dref_target_population_of_operation = validated_data.get('dref', instance.dref).total_targeted_population - dref_amount_requested = validated_data.get('dref', instance.dref).amount_requested - dref_district = validated_data.get('dref', instance.dref).district.all() - - if (not changing_timeframe_operation) and total_operation_timeframe and dref_operation_timeframe and\ - total_operation_timeframe != dref_operation_timeframe: - raise serializers.ValidationError('Found diffrent operation timeframe for dref and operational update with changing not set to true') - - if changing_timeframe_operation and total_operation_timeframe and dref_operation_timeframe and\ - total_operation_timeframe == dref_operation_timeframe: - raise serializers.ValidationError('Found same operation timeframe for dref and operational update with changing set to true') - - if (not changing_target_population_of_operation) and number_of_people_targeted and dref_target_population_of_operation and\ - dref_target_population_of_operation != number_of_people_targeted: - raise serializers.ValidationError('Found diffrent targeted population for dref and operational update with changing not set to true') - - if changing_target_population_of_operation and number_of_people_targeted and dref_target_population_of_operation and\ - dref_target_population_of_operation == number_of_people_targeted: - raise serializers.ValidationError('Found same targeted population for dref and operational update with changing set to true') + validated_data["updated_by"] = self.context["request"].user + # changing_timeframe_operation = validated_data.get( + # "changing_timeframe_operation", instance.changing_timeframe_operation + # ) + # total_operation_timeframe = validated_data.get("total_operation_timeframe", instance.total_operation_timeframe) + number_of_people_targeted = validated_data.get("number_of_people_targeted", instance.number_of_people_targeted) + request_for_second_allocation = validated_data.get( + "request_for_second_allocation", instance.request_for_second_allocation + ) + additional_allocation = validated_data.get("additional_allocation", instance.additional_allocation) + changing_target_population_of_operation = validated_data.get( + "changing_target_population_of_operation", instance.changing_target_population_of_operation + ) + changing_geographic_location = validated_data.get( + "changing_geographic_location", instance.changing_geographic_location + ) + district = validated_data.get("district", instance.district) + dref_operation_timeframe = validated_data.get("dref", instance.dref).operation_timeframe + dref_target_population_of_operation = validated_data.get("dref", instance.dref).total_targeted_population + dref_amount_requested = validated_data.get("dref", instance.dref).amount_requested + dref_district = validated_data.get("dref", instance.dref).district.all() + modified_at = validated_data.pop("modified_at", None) + if modified_at is None: + raise serializers.ValidationError({"modified_at": "Modified At is required!"}) + + # if ( + # (not changing_timeframe_operation) + # and total_operation_timeframe + # and dref_operation_timeframe + # and total_operation_timeframe != dref_operation_timeframe + # ): + # raise serializers.ValidationError( + # "Found diffrent operation timeframe for dref and operational update with changing not set to true" + # ) + + # if ( + # changing_timeframe_operation + # and total_operation_timeframe + # and dref_operation_timeframe + # and total_operation_timeframe == dref_operation_timeframe + # ): + # raise serializers.ValidationError( + # "Found same operation timeframe for dref and operational update with changing set to true" + # ) + + if ( + (not changing_target_population_of_operation) + and number_of_people_targeted + and dref_target_population_of_operation + and dref_target_population_of_operation != number_of_people_targeted + ): + raise serializers.ValidationError( + "Found diffrent targeted population for dref and operational update with changing not set to true" + ) + + if ( + changing_target_population_of_operation + and number_of_people_targeted + and dref_target_population_of_operation + and dref_target_population_of_operation == number_of_people_targeted + ): + raise serializers.ValidationError( + "Found same targeted population for dref and operational update with changing set to true" + ) # if request_for_second_allocation and additional_allocation and dref_amount_requested != additional_allocation: # raise serializers.ValidationError('Found diffrent allocation for dref and operation update') if (not changing_geographic_location) and district and dref_district and set(district) != set(dref_district): - raise serializers.ValidationError('Found different district for dref and operational update with changing not set to true') + raise serializers.ValidationError( + "Found different district for dref and operational update with changing not set to true" + ) if changing_geographic_location and district and dref_district and set(district) == set(dref_district): - raise serializers.ValidationError('Found same district for dref and operational update with changing set to true') + raise serializers.ValidationError( + "Found same district for dref and operational update with changing set to true" + ) + if modified_at and instance.modified_at and modified_at < instance.modified_at: + raise serializers.ValidationError({"modified_at": settings.DREF_OP_UPDATE_FINAL_REPORT_UPDATE_ERROR_MESSAGE}) return super().update(instance, validated_data) -class DrefFinalReportSerializer( - NestedUpdateMixin, - NestedCreateMixin, - serializers.ModelSerializer -): +class DrefFinalReportSerializer(NestedUpdateMixin, NestedCreateMixin, serializers.ModelSerializer): MAX_NUMBER_OF_PHOTOS = 2 national_society_actions = NationalSocietyActionSerializer(many=True, required=False) needs_identified = IdentifiedNeedSerializer(many=True, required=False) planned_interventions = PlannedInterventionSerializer(many=True, required=False) - type_of_onset_display = serializers.CharField(source='get_type_of_onset_display', read_only=True) - disaster_category_display = serializers.CharField(source='get_disaster_category_display', read_only=True) - created_by_details = UserNameSerializer(source='created_by', read_only=True) - event_map_details = DrefFileSerializer(source='event_map', read_only=True) - photos_details = DrefFileSerializer(source='photos', many=True, read_only=True) - country_details = MiniCountrySerializer(source='country', read_only=True) - district_details = MiniDistrictSerializer(source='district', read_only=True, many=True) - assessment_report_details = DrefFileSerializer(source='assessment_report', read_only=True) + type_of_onset_display = serializers.CharField(source="get_type_of_onset_display", read_only=True) + disaster_category_display = serializers.CharField(source="get_disaster_category_display", read_only=True) + created_by_details = UserNameSerializer(source="created_by", read_only=True) + event_map_file = DrefFileSerializer(source="event_map", required=False, allow_null=True) + cover_image_file = DrefFileSerializer(source="cover_image", required=False, allow_null=True) + images_file = DrefFileSerializer(many=True, required=False, allow_null=True, source="images") + photos_file = DrefFileSerializer(source="photos", many=True, required=False, allow_null=True) + country_details = MiniCountrySerializer(source="country", read_only=True) + district_details = MiniDistrictSerializer(source="district", read_only=True, many=True) + assessment_report_details = DrefFileSerializer(source="assessment_report", read_only=True) + risk_security = RiskSecuritySerializer(many=True, required=False) + modified_at = serializers.DateTimeField(required=False) + financial_report_details = DrefFileSerializer(source="financial_report", read_only=True) class Meta: model = DrefFinalReport - fields = '__all__' + exclude = ( + "images", + "photos", + "event_map", + "cover_image", + ) def validate(self, data): - dref = data.get('dref') + dref = data.get("dref") # check if dref is published and operational_update associated with it is also published if not self.instance and dref: if not dref.is_published: - raise serializers.ValidationError( - gettext('Can\'t create Final Report for not published dref %s.' % dref.id) - ) + raise serializers.ValidationError(gettext("Can't create Final Report for not published dref %s." % dref.id)) dref_operational_update = DrefOperationalUpdate.objects.filter( dref=dref, is_published=False, - ).values_list('id', flat=True) + ).values_list("id", flat=True) if dref_operational_update: raise serializers.ValidationError( gettext( - 'Can\'t create Final Report for not published Operational Update %s ids ' % ','.join(map(str, dref_operational_update)) + "Can't create Final Report for not published Operational Update %s ids " + % ",".join(map(str, dref_operational_update)) ) ) if self.instance and self.instance.is_published: - raise serializers.ValidationError( - gettext( - 'Can\'t update published final report %s.' % self.instance.id - ) - ) + raise serializers.ValidationError(gettext("Can't update published final report %s." % self.instance.id)) return data def validate_photos(self, photos): if photos and len(photos) > self.MAX_NUMBER_OF_PHOTOS: - raise serializers.ValidationError('Can add utmost %s photos' % self.MAX_NUMBER_OF_PHOTOS) + raise serializers.ValidationError("Can add utmost %s photos" % self.MAX_NUMBER_OF_PHOTOS) return photos def create(self, validated_data): # here check if there is operational update for corresponding dref # if yes copy from the latest operational update # else copy from dref - dref = validated_data['dref'] - dref_operational_update = DrefOperationalUpdate.objects.filter( - dref=dref, - ).order_by('-operational_update_number').first() + dref = validated_data["dref"] + dref_operational_update = ( + DrefOperationalUpdate.objects.filter(dref=dref, is_published=True).order_by("-operational_update_number").first() + ) if dref_operational_update: - validated_data['title'] = dref_operational_update.title - validated_data['title_prefix'] = dref_operational_update.title_prefix - validated_data['national_society'] = dref_operational_update.national_society - validated_data['disaster_type'] = dref_operational_update.disaster_type - validated_data['type_of_onset'] = dref_operational_update.type_of_onset - validated_data['disaster_category'] = dref_operational_update.disaster_category - validated_data['number_of_people_targeted'] = dref_operational_update.number_of_people_targeted - validated_data['number_of_people_affected'] = dref_operational_update.number_of_people_affected - validated_data['total_dref_allocation'] = dref_operational_update.total_dref_allocation - validated_data['total_operation_timeframe'] = dref_operational_update.total_operation_timeframe - validated_data['operation_start_date'] = dref_operational_update.dref.date_of_approval - validated_data['appeal_code'] = dref_operational_update.appeal_code - validated_data['glide_code'] = dref_operational_update.glide_code - validated_data['ifrc_appeal_manager_name'] = dref_operational_update.ifrc_appeal_manager_name - validated_data['ifrc_appeal_manager_email'] = dref_operational_update.ifrc_appeal_manager_email - validated_data['ifrc_appeal_manager_title'] = dref_operational_update.ifrc_appeal_manager_title - validated_data['ifrc_appeal_manager_phone_number'] = dref_operational_update.ifrc_appeal_manager_phone_number - validated_data['ifrc_project_manager_name'] = dref_operational_update.ifrc_project_manager_name - validated_data['ifrc_project_manager_email'] = dref_operational_update.ifrc_project_manager_email - validated_data['ifrc_project_manager_title'] = dref_operational_update.ifrc_project_manager_title - validated_data['ifrc_project_manager_phone_number'] = dref_operational_update.ifrc_project_manager_phone_number - validated_data['national_society_contact_name'] = dref_operational_update.national_society_contact_name - validated_data['national_society_contact_email'] = dref_operational_update.national_society_contact_email - validated_data['national_society_contact_title'] = dref_operational_update.national_society_contact_title - validated_data['national_society_contact_phone_number'] = dref_operational_update.national_society_contact_phone_number - validated_data['media_contact_name'] = dref_operational_update.media_contact_name - validated_data['media_contact_email'] = dref_operational_update.media_contact_email - validated_data['media_contact_title'] = dref_operational_update.media_contact_title - validated_data['media_contact_phone_number'] = dref_operational_update.media_contact_phone_number - validated_data['ifrc_emergency_name'] = dref_operational_update.ifrc_emergency_name - validated_data['ifrc_emergency_title'] = dref_operational_update.ifrc_emergency_title - validated_data['ifrc_emergency_phone_number'] = dref_operational_update.ifrc_emergency_phone_number - validated_data['ifrc_emergency_email'] = dref_operational_update.ifrc_emergency_email - validated_data['ifrc'] = dref_operational_update.ifrc - validated_data['icrc'] = dref_operational_update.icrc - validated_data['partner_national_society'] = dref_operational_update.partner_national_society - validated_data['government_requested_assistance'] = dref_operational_update.government_requested_assistance - validated_data['national_authorities'] = dref_operational_update.national_authorities - validated_data['un_or_other_actor'] = dref_operational_update.un_or_other_actor - validated_data['major_coordination_mechanism'] = dref_operational_update.major_coordination_mechanism - validated_data['people_assisted'] = dref_operational_update.people_assisted - validated_data['selection_criteria'] = dref_operational_update.selection_criteria - validated_data['entity_affected'] = dref_operational_update.entity_affected - validated_data['women'] = dref_operational_update.women - validated_data['men'] = dref_operational_update.men - validated_data['girls'] = dref_operational_update.girls - validated_data['boys'] = dref_operational_update.boys - validated_data['disability_people_per'] = dref_operational_update.disability_people_per - validated_data['people_per_urban'] = dref_operational_update.people_per_urban - validated_data['people_per_local'] = dref_operational_update.people_per_local - validated_data['people_targeted_with_early_actions'] = dref_operational_update.people_targeted_with_early_actions - validated_data['displaced_people'] = dref_operational_update.displaced_people - validated_data['operation_objective'] = dref_operational_update.operation_objective - validated_data['response_strategy'] = dref_operational_update.response_strategy - validated_data['created_by'] = self.context['request'].user - validated_data['operation_start_date'] = dref_operational_update.dref.date_of_approval - validated_data['event_description'] = dref_operational_update.event_description - validated_data['anticipatory_actions'] = dref_operational_update.anticipatory_actions - validated_data['event_scope'] = dref_operational_update.event_scope - validated_data['event_map'] = dref_operational_update.dref.event_map - validated_data['assessment_report'] = dref_operational_update.assessment_report - validated_data['country'] = dref_operational_update.country + validated_data["title"] = dref_operational_update.title + validated_data["title_prefix"] = dref_operational_update.title_prefix + validated_data["national_society"] = dref_operational_update.national_society + validated_data["disaster_type"] = dref_operational_update.disaster_type + validated_data["type_of_onset"] = dref_operational_update.type_of_onset + validated_data["disaster_category"] = dref_operational_update.disaster_category + validated_data["number_of_people_targeted"] = dref_operational_update.number_of_people_targeted + validated_data["number_of_people_affected"] = dref_operational_update.number_of_people_affected + validated_data["total_dref_allocation"] = dref_operational_update.total_dref_allocation + validated_data["total_operation_timeframe"] = dref_operational_update.total_operation_timeframe + validated_data["operation_start_date"] = dref_operational_update.dref.date_of_approval + validated_data["appeal_code"] = dref_operational_update.appeal_code + validated_data["glide_code"] = dref_operational_update.glide_code + validated_data["ifrc_appeal_manager_name"] = dref_operational_update.ifrc_appeal_manager_name + validated_data["ifrc_appeal_manager_email"] = dref_operational_update.ifrc_appeal_manager_email + validated_data["ifrc_appeal_manager_title"] = dref_operational_update.ifrc_appeal_manager_title + validated_data["ifrc_appeal_manager_phone_number"] = dref_operational_update.ifrc_appeal_manager_phone_number + validated_data["ifrc_project_manager_name"] = dref_operational_update.ifrc_project_manager_name + validated_data["ifrc_project_manager_email"] = dref_operational_update.ifrc_project_manager_email + validated_data["ifrc_project_manager_title"] = dref_operational_update.ifrc_project_manager_title + validated_data["ifrc_project_manager_phone_number"] = dref_operational_update.ifrc_project_manager_phone_number + validated_data["national_society_contact_name"] = dref_operational_update.national_society_contact_name + validated_data["national_society_contact_email"] = dref_operational_update.national_society_contact_email + validated_data["national_society_contact_title"] = dref_operational_update.national_society_contact_title + validated_data[ + "national_society_contact_phone_number" + ] = dref_operational_update.national_society_contact_phone_number + validated_data["media_contact_name"] = dref_operational_update.media_contact_name + validated_data["media_contact_email"] = dref_operational_update.media_contact_email + validated_data["media_contact_title"] = dref_operational_update.media_contact_title + validated_data["media_contact_phone_number"] = dref_operational_update.media_contact_phone_number + validated_data["ifrc_emergency_name"] = dref_operational_update.ifrc_emergency_name + validated_data["ifrc_emergency_title"] = dref_operational_update.ifrc_emergency_title + validated_data["ifrc_emergency_phone_number"] = dref_operational_update.ifrc_emergency_phone_number + validated_data["ifrc_emergency_email"] = dref_operational_update.ifrc_emergency_email + validated_data["ifrc"] = dref_operational_update.ifrc + validated_data["icrc"] = dref_operational_update.icrc + validated_data["partner_national_society"] = dref_operational_update.partner_national_society + validated_data["government_requested_assistance"] = dref_operational_update.government_requested_assistance + validated_data["national_authorities"] = dref_operational_update.national_authorities + validated_data["un_or_other_actor"] = dref_operational_update.un_or_other_actor + validated_data["major_coordination_mechanism"] = dref_operational_update.major_coordination_mechanism + validated_data["people_assisted"] = dref_operational_update.people_assisted + validated_data["selection_criteria"] = dref_operational_update.selection_criteria + validated_data["entity_affected"] = dref_operational_update.entity_affected + validated_data["women"] = dref_operational_update.women + validated_data["men"] = dref_operational_update.men + validated_data["girls"] = dref_operational_update.girls + validated_data["boys"] = dref_operational_update.boys + validated_data["disability_people_per"] = dref_operational_update.disability_people_per + validated_data["people_per_urban"] = dref_operational_update.people_per_urban + validated_data["people_per_local"] = dref_operational_update.people_per_local + validated_data["people_targeted_with_early_actions"] = dref_operational_update.people_targeted_with_early_actions + validated_data["displaced_people"] = dref_operational_update.displaced_people + validated_data["operation_objective"] = dref_operational_update.operation_objective + validated_data["response_strategy"] = dref_operational_update.response_strategy + validated_data["created_by"] = self.context["request"].user + validated_data["operation_start_date"] = dref_operational_update.dref.date_of_approval + validated_data["event_description"] = dref_operational_update.event_description + validated_data["anticipatory_actions"] = dref_operational_update.anticipatory_actions + validated_data["event_scope"] = dref_operational_update.event_scope + validated_data["country"] = dref_operational_update.country + validated_data["risk_security_concern"] = dref_operational_update.risk_security_concern + validated_data["is_assessment_report"] = dref_operational_update.is_assessment_report + validated_data["total_targeted_population"] = dref_operational_update.total_targeted_population + validated_data[ + "is_there_major_coordination_mechanism" + ] = dref_operational_update.is_there_major_coordination_mechanism + validated_data["event_date"] = dref_operational_update.event_date + validated_data["people_in_need"] = dref_operational_update.people_in_need + validated_data["ns_respond_date"] = dref_operational_update.ns_respond_date + validated_data["assessment_report"] = dref_operational_update.assessment_report dref_final_report = super().create(validated_data) + # XXX: Copy files from DREF (Only nested serialized fields) + nested_serialized_file_fields = [ + "cover_image", + "event_map", + ] + for file_field in nested_serialized_file_fields: + dref_file = getattr(dref, file_field, None) + if dref_file: + setattr(dref_final_report, file_field, dref_file.clone(self.context["request"].user)) + dref_final_report.save(update_fields=nested_serialized_file_fields) dref_final_report.planned_interventions.add(*dref_operational_update.planned_interventions.all()) dref_final_report.needs_identified.add(*dref_operational_update.needs_identified.all()) + dref_final_report.national_society_actions.add(*dref_operational_update.national_society_actions.all()) dref_final_report.district.add(*dref_operational_update.district.all()) + dref_final_report.images.add(*dref_operational_update.images.all()) + dref_final_report.photos.add(*dref_operational_update.photos.all()) + dref_final_report.risk_security.add(*dref_operational_update.risk_security.all()) + dref_final_report.users.add(*dref_operational_update.users.all()) else: - validated_data['title'] = dref.title - validated_data['title_prefix'] = dref.title_prefix - validated_data['national_society'] = dref.national_society - validated_data['disaster_type'] = dref.disaster_type - validated_data['type_of_onset'] = dref.type_of_onset - validated_data['disaster_category'] = dref.disaster_category - validated_data['number_of_people_targeted'] = dref.num_assisted - validated_data['number_of_people_affected'] = dref.num_affected - validated_data['total_operation_timeframe'] = dref.operation_timeframe - validated_data['operation_start_date'] = dref.date_of_approval - validated_data['appeal_code'] = dref.appeal_code - validated_data['glide_code'] = dref.glide_code - validated_data['ifrc_appeal_manager_name'] = dref.ifrc_appeal_manager_name - validated_data['ifrc_appeal_manager_email'] = dref.ifrc_appeal_manager_email - validated_data['ifrc_appeal_manager_title'] = dref.ifrc_appeal_manager_title - validated_data['ifrc_appeal_manager_phone_number'] = dref.ifrc_appeal_manager_phone_number - validated_data['ifrc_project_manager_name'] = dref.ifrc_project_manager_name - validated_data['ifrc_project_manager_email'] = dref.ifrc_project_manager_email - validated_data['ifrc_project_manager_title'] = dref.ifrc_project_manager_title - validated_data['ifrc_project_manager_phone_number'] = dref.ifrc_project_manager_phone_number - validated_data['national_society_contact_name'] = dref.national_society_contact_name - validated_data['national_society_contact_email'] = dref.national_society_contact_email - validated_data['national_society_contact_title'] = dref.national_society_contact_title - validated_data['national_society_contact_phone_number'] = dref.national_society_contact_phone_number - validated_data['media_contact_name'] = dref.media_contact_name - validated_data['media_contact_email'] = dref.media_contact_email - validated_data['media_contact_title'] = dref.media_contact_title - validated_data['media_contact_phone_number'] = dref.media_contact_phone_number - validated_data['ifrc_emergency_name'] = dref.ifrc_emergency_name - validated_data['ifrc_emergency_title'] = dref.ifrc_emergency_title - validated_data['ifrc_emergency_phone_number'] = dref.ifrc_emergency_phone_number - validated_data['ifrc_emergency_email'] = dref.ifrc_emergency_email - validated_data['ifrc'] = dref.ifrc - validated_data['icrc'] = dref.icrc - validated_data['partner_national_society'] = dref.partner_national_society - validated_data['government_requested_assistance'] = dref.government_requested_assistance - validated_data['national_authorities'] = dref.national_authorities - validated_data['un_or_other_actor'] = dref.un_or_other_actor - validated_data['major_coordination_mechanism'] = dref.major_coordination_mechanism - validated_data['people_assisted'] = dref.people_assisted - validated_data['selection_criteria'] = dref.selection_criteria - validated_data['entity_affected'] = dref.entity_affected - validated_data['women'] = dref.women - validated_data['men'] = dref.men - validated_data['girls'] = dref.girls - validated_data['boys'] = dref.boys - validated_data['disability_people_per'] = dref.disability_people_per - validated_data['people_per_urban'] = dref.people_per_urban - validated_data['people_per_local'] = dref.people_per_local - validated_data['people_targeted_with_early_actions'] = dref.people_targeted_with_early_actions - validated_data['displaced_people'] = dref.displaced_people - validated_data['operation_objective'] = dref.operation_objective - validated_data['response_strategy'] = dref.response_strategy - validated_data['created_by'] = self.context['request'].user - validated_data['event_description'] = dref.event_description - validated_data['anticipatory_actions'] = dref.anticipatory_actions - validated_data['event_scope'] = dref.event_scope - validated_data['event_map'] = dref.event_map - validated_data['assessment_report'] = dref.assessment_report - validated_data['country'] = dref.country + validated_data["title"] = dref.title + validated_data["title_prefix"] = dref.title_prefix + validated_data["national_society"] = dref.national_society + validated_data["disaster_type"] = dref.disaster_type + validated_data["type_of_onset"] = dref.type_of_onset + validated_data["disaster_category"] = dref.disaster_category + validated_data["number_of_people_targeted"] = dref.num_assisted + validated_data["number_of_people_affected"] = dref.num_affected + validated_data["total_operation_timeframe"] = dref.operation_timeframe + validated_data["operation_start_date"] = dref.date_of_approval + validated_data["appeal_code"] = dref.appeal_code + validated_data["glide_code"] = dref.glide_code + validated_data["ifrc_appeal_manager_name"] = dref.ifrc_appeal_manager_name + validated_data["ifrc_appeal_manager_email"] = dref.ifrc_appeal_manager_email + validated_data["ifrc_appeal_manager_title"] = dref.ifrc_appeal_manager_title + validated_data["ifrc_appeal_manager_phone_number"] = dref.ifrc_appeal_manager_phone_number + validated_data["ifrc_project_manager_name"] = dref.ifrc_project_manager_name + validated_data["ifrc_project_manager_email"] = dref.ifrc_project_manager_email + validated_data["ifrc_project_manager_title"] = dref.ifrc_project_manager_title + validated_data["ifrc_project_manager_phone_number"] = dref.ifrc_project_manager_phone_number + validated_data["national_society_contact_name"] = dref.national_society_contact_name + validated_data["national_society_contact_email"] = dref.national_society_contact_email + validated_data["national_society_contact_title"] = dref.national_society_contact_title + validated_data["national_society_contact_phone_number"] = dref.national_society_contact_phone_number + validated_data["media_contact_name"] = dref.media_contact_name + validated_data["media_contact_email"] = dref.media_contact_email + validated_data["media_contact_title"] = dref.media_contact_title + validated_data["media_contact_phone_number"] = dref.media_contact_phone_number + validated_data["ifrc_emergency_name"] = dref.ifrc_emergency_name + validated_data["ifrc_emergency_title"] = dref.ifrc_emergency_title + validated_data["ifrc_emergency_phone_number"] = dref.ifrc_emergency_phone_number + validated_data["ifrc_emergency_email"] = dref.ifrc_emergency_email + validated_data["ifrc"] = dref.ifrc + validated_data["icrc"] = dref.icrc + validated_data["partner_national_society"] = dref.partner_national_society + validated_data["government_requested_assistance"] = dref.government_requested_assistance + validated_data["national_authorities"] = dref.national_authorities + validated_data["un_or_other_actor"] = dref.un_or_other_actor + validated_data["major_coordination_mechanism"] = dref.major_coordination_mechanism + validated_data["people_assisted"] = dref.people_assisted + validated_data["selection_criteria"] = dref.selection_criteria + validated_data["entity_affected"] = dref.entity_affected + validated_data["women"] = dref.women + validated_data["men"] = dref.men + validated_data["girls"] = dref.girls + validated_data["boys"] = dref.boys + validated_data["disability_people_per"] = dref.disability_people_per + validated_data["people_per_urban"] = dref.people_per_urban + validated_data["people_per_local"] = dref.people_per_local + validated_data["people_targeted_with_early_actions"] = dref.people_targeted_with_early_actions + validated_data["displaced_people"] = dref.displaced_people + validated_data["operation_objective"] = dref.operation_objective + validated_data["response_strategy"] = dref.response_strategy + validated_data["created_by"] = self.context["request"].user + validated_data["event_description"] = dref.event_description + validated_data["anticipatory_actions"] = dref.anticipatory_actions + validated_data["event_scope"] = dref.event_scope + validated_data["event_map"] = dref.event_map + validated_data["assessment_report"] = dref.assessment_report + validated_data["country"] = dref.country + validated_data["risk_security_concern"] = dref.risk_security_concern + validated_data["is_assessment_report"] = dref.is_assessment_report + validated_data["total_targeted_population"] = dref.total_targeted_population + validated_data["is_there_major_coordination_mechanism"] = dref.is_there_major_coordination_mechanism + validated_data["event_date"] = dref.event_date + validated_data["people_in_need"] = dref.people_in_need + validated_data["event_text"] = dref.event_text + validated_data["ns_respond_date"] = dref.ns_respond_date + validated_data["cover_image"] = dref.cover_image + validated_data["event_map"] = dref.event_map dref_final_report = super().create(validated_data) + # XXX: Copy files from DREF (Only nested serialized fields) + nested_serialized_file_fields = [ + "cover_image", + "event_map", + ] + for file_field in nested_serialized_file_fields: + dref_file = getattr(dref, file_field, None) + if dref_file: + setattr(dref_final_report, file_field, dref_file.clone(self.context["request"].user)) + dref_final_report.save(update_fields=nested_serialized_file_fields) dref_final_report.planned_interventions.add(*dref.planned_interventions.all()) dref_final_report.needs_identified.add(*dref.needs_identified.all()) dref_final_report.district.add(*dref.district.all()) + dref_final_report.images.add(*dref.images.all()) + dref_final_report.risk_security.add(*dref.risk_security.all()) + dref_final_report.users.add(*dref.users.all()) + dref_final_report.national_society_actions.add(*dref.national_society_actions.all()) # also update is_final_report_created for dref dref.is_final_report_created = True - dref.save(update_fields=['is_final_report_created']) + dref.save(update_fields=["is_final_report_created"]) return dref_final_report def update(self, instance, validated_data): - validated_data['updated_by'] = self.context['request'].user + modified_at = validated_data.pop("modified_at", None) + if modified_at is None: + raise serializers.ValidationError({"modified_at": "Modified At is required!"}) + if modified_at and instance.modified_at and modified_at < instance.modified_at: + raise serializers.ValidationError({"modified_at": settings.DREF_OP_UPDATE_FINAL_REPORT_UPDATE_ERROR_MESSAGE}) + + validated_data["updated_by"] = self.context["request"].user return super().update(instance, validated_data) diff --git a/dref/tasks.py b/dref/tasks.py index 188c03143..081c156d8 100644 --- a/dref/tasks.py +++ b/dref/tasks.py @@ -6,14 +6,14 @@ @shared_task -def send_dref_email(dref_id, users_emails, new_or_updated=''): +def send_dref_email(dref_id, users_emails, new_or_updated=""): if dref_id and users_emails: instance = Dref.objects.get(id=dref_id) email_context = get_email_context(instance) send_notification( - f'{new_or_updated} DREF: {instance.title}', + f"{new_or_updated} DREF: {instance.title}", users_emails, - render_to_string('email/dref/dref.html', email_context), - f'{new_or_updated} DREF' + render_to_string("email/dref/dref.html", email_context), + f"{new_or_updated} DREF", ) return email_context diff --git a/dref/templates/search/indexes/dref/dref_text.txt b/dref/templates/search/indexes/dref/dref_text.txt new file mode 100644 index 000000000..2fad18d5f --- /dev/null +++ b/dref/templates/search/indexes/dref/dref_text.txt @@ -0,0 +1 @@ +{{object.name}} diff --git a/dref/templates/search/indexes/dref/drefoperationalupdate_text.txt b/dref/templates/search/indexes/dref/drefoperationalupdate_text.txt new file mode 100644 index 000000000..2fad18d5f --- /dev/null +++ b/dref/templates/search/indexes/dref/drefoperationalupdate_text.txt @@ -0,0 +1 @@ +{{object.name}} diff --git a/dref/test_views.py b/dref/test_views.py index 7a5ecc497..05d64d893 100644 --- a/dref/test_views.py +++ b/dref/test_views.py @@ -21,11 +21,7 @@ from deployments.factories.user import UserFactory -from api.models import ( - Country, - DisasterType, - District -) +from api.models import Country, DisasterType, District from dref.tasks import send_dref_email @@ -33,66 +29,56 @@ class DrefTestCase(APITestCase): def setUp(self): super().setUp() - path = os.path.join(settings.TEST_DIR, 'documents') - self.file = os.path.join(path, 'go.png') + path = os.path.join(settings.TEST_DIR, "documents") + self.file = os.path.join(path, "go.png") def test_upload_file(self): file_count = DrefFile.objects.count() - url = '/api/v2/dref-files/' + url = "/api/v2/dref-files/" data = { - 'file': open(self.file, 'rb'), + "file": open(self.file, "rb"), } self.authenticate() - response = self.client.post(url, data, format='multipart') + response = self.client.post(url, data, format="multipart") self.assert_201(response) self.assertEqual(DrefFile.objects.count(), file_count + 1) def test_upload_multiple_file(self): file_count = DrefFile.objects.count() - url = '/api/v2/dref-files/multiple/' - data = { - 'file': [open(self.file, 'rb'), open(self.file, 'rb'), open(self.file, 'rb')] - } + url = "/api/v2/dref-files/multiple/" + data = {"file": [open(self.file, "rb"), open(self.file, "rb"), open(self.file, "rb")]} self.authenticate() - response = self.client.post(url, data, format='multipart') + response = self.client.post(url, data, format="multipart") self.assert_201(response) self.assertEqual(DrefFile.objects.count(), file_count + 3) def test_upload_invalid_files(self): file_count = DrefFile.objects.count() - url = '/api/v2/dref-files/multiple/' - data = { - 'file': [open(self.file, 'rb'), open(self.file, 'rb'), open(self.file, 'rb'), "test_string"] - } + url = "/api/v2/dref-files/multiple/" + data = {"file": [open(self.file, "rb"), open(self.file, "rb"), open(self.file, "rb"), "test_string"]} self.authenticate() - response = self.client.post(url, data, format='multipart') + response = self.client.post(url, data, format="multipart") self.assert_400(response) self.assertEqual(DrefFile.objects.count(), file_count) # no new files to be created def test_dref_file(self): file1, file2, file3, file5 = DrefFileFactory.create_batch(4, created_by=self.user) file4 = DrefFileFactory.create(created_by=self.ifrc_user) - url = '/api/v2/dref-files/' + url = "/api/v2/dref-files/" self.client.force_authenticate(self.user) response = self.client.get(url) self.assert_200(response) - self.assertEqual(len(response.data['results']), 4) - self.assertEqual( - set(file['id'] for file in response.data['results']), - set([file1.id, file2.id, file3.id, file5.id]) - ) + self.assertEqual(len(response.data["results"]), 4) + self.assertEqual(set(file["id"] for file in response.data["results"]), set([file1.id, file2.id, file3.id, file5.id])) # authenticate with another user self.client.force_authenticate(self.ifrc_user) response = self.client.get(url) self.assert_200(response) - self.assertEqual(len(response.data['results']), 1) - self.assertEqual( - set(file['id'] for file in response.data['results']), - set([file4.id]) - ) + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(set(file["id"] for file in response.data["results"]), set([file4.id])) def test_get_dref(self): """ @@ -101,30 +87,30 @@ def test_get_dref(self): # create a dref dref_1 = DrefFactory.create(created_by=self.user) dref_1.users.add(self.ifrc_user) - url = '/api/v2/dref/' + url = "/api/v2/dref/" self.client.force_authenticate(self.user) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.data['results']), 1) + self.assertEqual(len(resp.data["results"]), 1) # authenticate with another user and try to view the dref self.client.force_authenticate(self.ifrc_user) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.data['results']), 1) + self.assertEqual(len(resp.data["results"]), 1) # try to get the dref by user who neither created nor has access to dref user = UserFactory.create() self.client.force_authenticate(user) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) - self.assertEqual(len(resp.data['results']), 0) + self.assertEqual(len(resp.data["results"]), 0) - @mock.patch('notifications.notification.send_notification') + @mock.patch("notifications.notification.send_notification") def test_post_dref_creation(self, send_notification): old_count = Dref.objects.count() - national_society = Country.objects.create(name='xzz') - disaster_type = DisasterType.objects.create(name='abc') + national_society = Country.objects.create(name="xzz") + disaster_type = DisasterType.objects.create(name="abc") data = { "title": "Dref test title", "type_of_onset": Dref.OnsetType.SLOW.value, @@ -186,12 +172,7 @@ def test_post_dref_creation(self, send_notification): "originator_email": "test@gmail.com", "national_society": national_society.id, "disaster_type": disaster_type.id, - "needs_identified": [ - { - "title": "environment_sustainability ", - "description": "hey" - } - ], + "needs_identified": [{"title": "environment_sustainability ", "description": "hey"}], "planned_interventions": [ { "title": "shelter_housing_and_settlements", @@ -201,11 +182,11 @@ def test_post_dref_creation(self, send_notification): "female": 2255, "indicators": [ { - 'title': "test_title", - 'actual': 21232, - 'target': 44444, + "title": "test_title", + "actual": 21232, + "target": 44444, } - ] + ], }, { "id": 2, @@ -216,33 +197,33 @@ def test_post_dref_creation(self, send_notification): "female": 2255, "indicators": [ { - 'title': "test_title", - 'actual': 21232, - 'target': 44444, + "title": "test_title", + "actual": 21232, + "target": 44444, } - ] - } + ], + }, ], - 'users': [self.user.id] + "users": [self.user.id], } - url = '/api/v2/dref/' + url = "/api/v2/dref/" self.client.force_authenticate(self.user) - response = self.client.post(url, data, format='json') + response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 201) self.assertEqual(Dref.objects.count(), old_count + 1) - instance = Dref.objects.get(id=response.data['id']) + instance = Dref.objects.get(id=response.data["id"]) instance_user_email = [user.email for user in instance.users.all()] # call email send task email_data = send_dref_email(instance.id, instance_user_email) self.assertTrue(send_notification.assert_called) - self.assertEqual(email_data['title'], instance.title) + self.assertEqual(email_data["title"], instance.title) def test_event_date_in_dref(self): """ Test for the event date based on type_of_onset """ - national_society = Country.objects.create(name='xzz') + national_society = Country.objects.create(name="xzz") data = { "title": "Dref test title", "type_of_onset": Dref.OnsetType.SLOW.value, @@ -303,40 +284,26 @@ def test_event_date_in_dref(self): "ifrc_emergency_email": "test@gmail.com", "originator_name": "Test Name", "originator_email": "test@gmail.com", - "needs_identified": [ - { - "title": "environment_sustainability ", - "description": "hey" - } - ], + "needs_identified": [{"title": "environment_sustainability ", "description": "hey"}], "planned_interventions": [ { "title": "shelter_housing_and_settlements", "description": "matrix", "budget": 23444, - "person_targeted": 12222 + "person_targeted": 12222, }, - { - "id": 2, - "title": "health", - "description": "matrix reloaded", - "budget": 451111111, - "person_targeted": 345 - } + {"id": 2, "title": "health", "description": "matrix reloaded", "budget": 451111111, "person_targeted": 345}, ], "images_file": [], "cover_image_file": None, } - url = '/api/v2/dref/' + url = "/api/v2/dref/" self.client.force_authenticate(self.user) - response = self.client.post(url, data, format='json') + response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 201) - id = response.data['id'] - url = f'/api/v2/dref/{id}/' - data = { - "type_of_onset": Dref.OnsetType.IMMINENT.value, - "event_date": "2020-10-10" - } + id = response.data["id"] + url = f"/api/v2/dref/{id}/" + data = {"type_of_onset": Dref.OnsetType.IMMINENT.value, "event_date": "2020-10-10"} response = self.client.patch(url, data) self.assertEqual(response.status_code, 400) @@ -345,19 +312,10 @@ def test_update_dref_image(self): file4 = DrefFileFactory.create(created_by=self.ifrc_user) dref = DrefFactory.create(created_by=self.user) dref.users.add(self.ifrc_user) - url = f'/api/v2/dref/{dref.id}/' + url = f"/api/v2/dref/{dref.id}/" data = { - "images_file": [ - { - "file": file1.id, - "caption": "Test Caption" - }, - { - "file": file2.id, - "caption": "Test Caption" - } - ], - "modified_at": datetime.now() + "images_file": [{"file": file1.id, "caption": "Test Caption"}, {"file": file2.id, "caption": "Test Caption"}], + "modified_at": datetime.now(), } self.client.force_authenticate(self.user) response = self.client.patch(url, data=data, format="json") @@ -365,46 +323,21 @@ def test_update_dref_image(self): # now remove one file and add one file by `self.ifrc_user` data = { - "images_file": [ - { - "file": file1.id, - "caption": "Test Caption" - }, - { - "file": file4.id, - "caption": "Test Caption" - } - ], - "modified_at": datetime.now() + "images_file": [{"file": file1.id, "caption": "Test Caption"}, {"file": file4.id, "caption": "Test Caption"}], + "modified_at": datetime.now(), } self.client.force_authenticate(self.ifrc_user) - response = self.client.patch(url, data, format='multipart') + response = self.client.patch(url, data, format="multipart") self.assert_200(response) # add from another user - data = { - "images_file": [ - { - "file": file4.id, - "caption": "Test Caption" - } - ], - "modified_at": datetime.now() - } + data = {"images_file": [{"file": file4.id, "caption": "Test Caption"}], "modified_at": datetime.now()} self.client.force_authenticate(self.ifrc_user) response = self.client.patch(url, data) self.assert_200(response) # add file created_by another user - data = { - "images_file": [ - { - "file": file5.id, - "caption": "Test Caption" - } - ], - "modified_at": datetime.now() - } + data = {"images_file": [{"file": file5.id, "caption": "Test Caption"}], "modified_at": datetime.now()} self.client.force_authenticate(self.ifrc_user) response = self.client.patch(url, data) # self.assert_400(response) @@ -413,35 +346,49 @@ def test_filter_dref_status(self): """ Test to filter dref status """ - DrefFactory.create(title='test', status=Dref.Status.COMPLETED, date_of_approval='2020-10-10', created_by=self.user) - DrefFactory.create(status=Dref.Status.COMPLETED, date_of_approval='2020-10-10', created_by=self.user) - DrefFactory.create(status=Dref.Status.COMPLETED, date_of_approval='2020-10-10', created_by=self.user) + DrefFactory.create(title="test", status=Dref.Status.COMPLETED, date_of_approval="2020-10-10", created_by=self.user) + DrefFactory.create(status=Dref.Status.COMPLETED, date_of_approval="2020-10-10", created_by=self.user) + DrefFactory.create(status=Dref.Status.COMPLETED, date_of_approval="2020-10-10", created_by=self.user) DrefFactory.create(status=Dref.Status.IN_PROGRESS, created_by=self.user) DrefFactory.create(status=Dref.Status.IN_PROGRESS, created_by=self.user) # filter by `In Progress` - url = f'/api/v2/dref/?status={Dref.Status.IN_PROGRESS.value}' + url = f"/api/v2/dref/?status={Dref.Status.IN_PROGRESS.value}" + self.client.force_authenticate(self.user) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data["results"]), 2) + + def test_dref_country_filter(self): + country1 = Country.objects.create(name="country1") + country2 = Country.objects.create(name="country2") + DrefFactory.create(title="test", status=Dref.Status.COMPLETED, created_by=self.user, country=country1) + DrefFactory.create(status=Dref.Status.COMPLETED, created_by=self.user) + DrefFactory.create(status=Dref.Status.COMPLETED, created_by=self.user, country=country2) + DrefFactory.create(status=Dref.Status.IN_PROGRESS, created_by=self.user, country=country1) + DrefFactory.create(status=Dref.Status.IN_PROGRESS, created_by=self.user) + url = f"/api/v2/dref/?country={country1.id}" self.client.force_authenticate(self.user) response = self.client.get(url) self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data['results']), 2) + self.assertEqual(len(response.data["results"]), 2) def test_dref_options(self): """ Test for various dref attributes """ - url = '/api/v2/dref-options/' + url = "/api/v2/dref-options/" self.client.force_authenticate(self.user) response = self.client.get(url) self.assertEqual(response.status_code, 200) - self.assertIn('status', response.data) - self.assertIn('type_of_onset', response.data) - self.assertIn('disaster_category', response.data) - self.assertIn('planned_interventions', response.data) - self.assertIn('needs_identified', response.data) - self.assertIn('national_society_actions', response.data) - - @mock.patch('django.utils.timezone.now') + self.assertIn("status", response.data) + self.assertIn("type_of_onset", response.data) + self.assertIn("disaster_category", response.data) + self.assertIn("planned_interventions", response.data) + self.assertIn("needs_identified", response.data) + self.assertIn("national_society_actions", response.data) + + @mock.patch("django.utils.timezone.now") def test_dref_is_published(self, mock_now): """ Test for dref if is_published = True @@ -450,11 +397,11 @@ def test_dref_is_published(self, mock_now): mock_now.return_value = initial_now dref = DrefFactory.create( - title='test', + title="test", created_by=self.user, is_published=True, ) - url = f'/api/v2/dref/{dref.id}/' + url = f"/api/v2/dref/{dref.id}/" data = { "title": "New Update Title", "modified_at": initial_now, @@ -465,25 +412,25 @@ def test_dref_is_published(self, mock_now): # create new dref with is_published = False not_published_dref = DrefFactory.create( - title='test', + title="test", created_by=self.user, ) - url = f'/api/v2/dref/{not_published_dref.id}/' + url = f"/api/v2/dref/{not_published_dref.id}/" self.client.force_authenticate(self.user) response = self.client.patch(url, data) self.assert_200(response) - data['modified_at'] = initial_now - timedelta(seconds=10) + data["modified_at"] = initial_now - timedelta(seconds=10) response = self.client.patch(url, data) self.assert_400(response) # test dref published endpoint - url = f'/api/v2/dref/{not_published_dref.id}/publish/' + url = f"/api/v2/dref/{not_published_dref.id}/publish/" data = {} self.client.force_authenticate(self.user) response = self.client.post(url, data) self.assert_200(response) - self.assertEqual(response.data['is_published'], True) + self.assertEqual(response.data["is_published"], True) def test_dref_operation_update_create(self): """ @@ -491,18 +438,19 @@ def test_dref_operation_update_create(self): """ user1, _ = UserFactory.create_batch(2) dref = DrefFactory.create( - title='Test Title', created_by=self.user, + title="Test Title", + created_by=self.user, is_published=True, ) dref.users.add(user1) - self.country1 = Country.objects.create(name='abc') - self.district1 = District.objects.create(name='test district1', country=self.country1) + self.country1 = Country.objects.create(name="abc") + self.district1 = District.objects.create(name="test district1", country=self.country1) old_count = DrefOperationalUpdate.objects.count() - url = '/api/v2/dref-op-update/' + url = "/api/v2/dref-op-update/" data = { - 'dref': dref.id, - 'country': self.country1.id, - 'district': [self.district1.id], + "dref": dref.id, + "country": self.country1.id, + "district": [self.district1.id], } self.authenticate(self.user) response = self.client.post(url, data=data) @@ -513,14 +461,13 @@ def test_dref_operation_update_for_published_dref(self): # NOTE: If DREF is not published can't create Operational Update user1, _ = UserFactory.create_batch(2) dref = DrefFactory.create( - title='Test Title', created_by=self.user, + title="Test Title", + created_by=self.user, is_published=False, ) dref.users.add(user1) - url = '/api/v2/dref-op-update/' - data = { - 'dref': dref.id - } + url = "/api/v2/dref-op-update/" + data = {"dref": dref.id} self.client.force_authenticate(user1) response = self.client.post(url, data=data) self.assert_400(response) @@ -528,40 +475,34 @@ def test_dref_operation_update_for_published_dref(self): def test_dref_operational_create_for_parent(self): user1, _ = UserFactory.create_batch(2) dref = DrefFactory.create( - title='Test Title', created_by=self.user, + title="Test Title", + created_by=self.user, is_published=True, ) dref.users.add(user1) - DrefOperationalUpdateFactory.create( - dref=dref, - is_published=True, - operational_update_number=1 - ) + DrefOperationalUpdateFactory.create(dref=dref, is_published=True, operational_update_number=1) data = { - 'dref': dref.id, + "dref": dref.id, } - url = '/api/v2/dref-op-update/' + url = "/api/v2/dref-op-update/" self.authenticate(user1) response = self.client.post(url, data) self.assert_201(response) - self.assertEqual(response.data['operational_update_number'], 2) + self.assertEqual(response.data["operational_update_number"], 2) def test_operational_update_create_for_not_published_parent(self): user1, _ = UserFactory.create_batch(2) dref = DrefFactory.create( - title='Test Title', created_by=self.user, + title="Test Title", + created_by=self.user, is_published=True, ) dref.users.add(user1) - DrefOperationalUpdateFactory.create( - dref=dref, - is_published=False, - operational_update_number=1 - ) + DrefOperationalUpdateFactory.create(dref=dref, is_published=False, operational_update_number=1) data = { - 'dref': dref.id, + "dref": dref.id, } - url = '/api/v2/dref-op-update/' + url = "/api/v2/dref-op-update/" self.client.force_authenticate(user1) response = self.client.post(url, data) self.assert_400(response) @@ -569,53 +510,47 @@ def test_operational_update_create_for_not_published_parent(self): def test_dref_operational_update_patch(self): user1, _ = UserFactory.create_batch(2) dref = DrefFactory.create( - title='Test Title', created_by=user1, + title="Test Title", + created_by=user1, is_published=True, ) dref.users.add(user1) - DrefOperationalUpdateFactory.create( - dref=dref, - is_published=True, - operational_update_number=1 - ) - url = '/api/v2/dref-op-update/' + DrefOperationalUpdateFactory.create(dref=dref, is_published=True, operational_update_number=1) + url = "/api/v2/dref-op-update/" data = { - 'dref': dref.id, + "dref": dref.id, } self.authenticate(user=user1) response = self.client.post(url, data=data) self.assert_201(response) - response_id = response.data['id'] + response_id = response.data["id"] data = { - 'title': 'New Operation title', - 'new_operational_end_date': '2022-10-10', - 'reporting_timeframe': '2022-10-16', - 'is_timeframe_extension_required': True, + "title": "New Operation title", + "new_operational_end_date": "2022-10-10", + "reporting_timeframe": "2022-10-16", + "is_timeframe_extension_required": True, + "modified_at": datetime.now() + timedelta(days=12), } - url = f'/api/v2/dref-op-update/{response_id}/' + url = f"/api/v2/dref-op-update/{response_id}/" self.authenticate(user=user1) response = self.client.patch(url, data) self.assert_200(response) def test_dref_change_on_final_report_create(self): user1 = UserFactory.create() - dref = DrefFactory.create( - title='Test Title', - created_by=user1, - is_final_report_created=True - ) + dref = DrefFactory.create(title="Test Title", created_by=user1, is_final_report_created=True) DrefFinalReportFactory.create( dref=dref, is_published=True, ) # try to patch to dref data = { - 'title': "hey title", - 'new_operational_end_date': '2022-10-10', - 'reporting_timeframe': '2022-10-16', - 'is_timeframe_extension_required': True, + "title": "hey title", + "new_operational_end_date": "2022-10-10", + "reporting_timeframe": "2022-10-16", + "is_timeframe_extension_required": True, } - url = f'/api/v2/dref/{dref.id}/' + url = f"/api/v2/dref/{dref.id}/" self.authenticate(user=user1) response = self.client.patch(url, data) self.assert_400(response) @@ -623,20 +558,21 @@ def test_dref_change_on_final_report_create(self): def test_dref_fields_copied_to_final_report(self): user1, _ = UserFactory.create_batch(2) dref = DrefFactory.create( - title='Test Title', created_by=self.user, + title="Test Title", + created_by=self.user, is_published=True, ) dref.users.add(user1) old_count = DrefFinalReport.objects.count() - url = '/api/v2/dref-final-report/' + url = "/api/v2/dref-final-report/" data = { - 'dref': dref.id, + "dref": dref.id, } self.authenticate(self.user) response = self.client.post(url, data=data) self.assert_201(response) self.assertEqual(DrefFinalReport.objects.count(), old_count + 1) - self.assertEqual(response.data['title'], dref.title) + self.assertEqual(response.data["title"], dref.title) def test_dref_operational_update_copied_to_final_report(self): """ @@ -645,62 +581,52 @@ def test_dref_operational_update_copied_to_final_report(self): """ user1, _ = UserFactory.create_batch(2) dref = DrefFactory.create( - title='Test Title', created_by=user1, + title="Test Title", + created_by=user1, is_published=True, ) dref.users.add(user1) operational_update = DrefOperationalUpdateFactory.create( - dref=dref, - title='Operational Update Title', - is_published=False, - operational_update_number=1 + dref=dref, title="Operational Update Title", is_published=False, operational_update_number=1 ) old_count = DrefFinalReport.objects.count() - url = '/api/v2/dref-final-report/' + url = "/api/v2/dref-final-report/" data = { - 'dref': dref.id, + "dref": dref.id, } self.authenticate(self.user) response = self.client.post(url, data=data) self.assert_400(response) # update the operational_update operational_update.is_published = True - operational_update.save(update_fields=['is_published']) + operational_update.save(update_fields=["is_published"]) response = self.client.post(url, data=data) self.assert_201(response) self.assertEqual(DrefFinalReport.objects.count(), old_count + 1) - self.assertEqual(response.data['title'], operational_update.title) + self.assertEqual(response.data["title"], operational_update.title) def test_final_report_for_dref(self): # here a final report is already created for dref # no multiple final report allowed for a dref user1 = UserFactory.create() dref = DrefFactory.create( - title='Test Title', + title="Test Title", created_by=user1, ) DrefFinalReportFactory.create( dref=dref, ) - url = '/api/v2/dref-final-report/' - data = { - 'dref': dref.id - } + url = "/api/v2/dref-final-report/" + data = {"dref": dref.id} self.authenticate(self.user) response = self.client.post(url, data) self.assert_400(response) def test_update_dref_for_final_report_created(self): user1 = UserFactory.create() - dref = DrefFactory.create( - title='Test Title', - created_by=user1, - is_published=True - ) - url = '/api/v2/dref-final-report/' - data = { - 'dref': dref.id - } + dref = DrefFactory.create(title="Test Title", created_by=user1, is_published=True) + url = "/api/v2/dref-final-report/" + data = {"dref": dref.id} self.authenticate(self.user) response = self.client.post(url, data) self.assert_201(response) @@ -709,34 +635,36 @@ def test_update_dref_for_final_report_created(self): def test_final_report_update_once_published(self): user1 = UserFactory.create() + dref = DrefFactory.create(title="Test Title", created_by=user1, is_published=True) final_report = DrefFinalReportFactory( - title='Test title', - created_by=user1, + title="Test title", + dref=dref, ) + final_report.users.set([user1]) # try to publish this report - url = f'/api/v2/dref-final-report/{final_report.id}/publish/' + url = f"/api/v2/dref-final-report/{final_report.id}/publish/" data = {} - self.client.force_authenticate(self.user) + self.client.force_authenticate(user1) response = self.client.post(url, data) self.assert_200(response) - self.assertEqual(response.data['is_published'], True) + self.assertEqual(response.data["is_published"], True) # againt try to publish final response = self.client.post(url, data) self.assert_400(response) # now try to patch to the final report - url = f'/api/v2/dref-final-report/{final_report.id}/' + url = f"/api/v2/dref-final-report/{final_report.id}/" data = { - 'title': 'New Field Report Title', + "title": "New Field Report Title", } response = self.client.patch(url, data) self.assert_400(response) def test_dref_for_assessment_report(self): old_count = Dref.objects.count() - national_society = Country.objects.create(name='xzz') - disaster_type = DisasterType.objects.create(name='abc') + national_society = Country.objects.create(name="xzz") + disaster_type = DisasterType.objects.create(name="abc") data = { "title": "Dref test title", "type_of_onset": Dref.OnsetType.SLOW.value, @@ -799,14 +727,9 @@ def test_dref_for_assessment_report(self): "national_society": national_society.id, "disaster_type": disaster_type.id, "is_assessment_report": True, - "needs_identified": [ - { - "title": "environment_sustainability ", - "description": "hey" - } - ], + "needs_identified": [{"title": "environment_sustainability ", "description": "hey"}], } - url = '/api/v2/dref/' + url = "/api/v2/dref/" self.client.force_authenticate(self.user) response = self.client.post(url, data) self.assertEqual(response.status_code, 201) @@ -814,65 +737,63 @@ def test_dref_for_assessment_report(self): def test_dref_for_super_user(self): user1 = UserFactory.create( - username='user1@test.com', - first_name='Test', - last_name='User1', - password='admin123', - email='user1@test.com', + username="user1@test.com", + first_name="Test", + last_name="User1", + password="admin123", + email="user1@test.com", is_superuser=True, ) user2 = UserFactory.create( - username='user2@test.com', - first_name='Test', - last_name='User2', - password='admin123', - email='user2@test.com', + username="user2@test.com", + first_name="Test", + last_name="User2", + password="admin123", + email="user2@test.com", ) user3 = UserFactory.create( - username='user3@test.com', - first_name='Test', - last_name='User3', - password='admin123', - email='user3@test.com', + username="user3@test.com", + first_name="Test", + last_name="User3", + password="admin123", + email="user3@test.com", ) dref1 = DrefFactory.create( - title='Test Title', + title="Test Title", created_by=user2, ) DrefFactory.create( - title='Test Title New', + title="Test Title New", ) # authenticate with user1(superuser) # user1 should be able to view all dref - url = '/api/v2/dref/' + url = "/api/v2/dref/" self.client.force_authenticate(user1) response = self.client.get(url) self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data['results']), 2) + self.assertEqual(len(response.data["results"]), 2) # authenticate with User2 self.client.force_authenticate(user2) response = self.client.get(url) self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data['results']), 1) - self.assertEqual(response.data['results'][0]['id'], dref1.id) + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], dref1.id) # authenticate with User3 self.client.force_authenticate(user3) response = self.client.get(url) self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data['results']), 0) + self.assertEqual(len(response.data["results"]), 0) def test_dref_latest_update(self): dref = DrefFactory.create( - title='Test Title', - created_by=self.user, - modified_at=datetime(2022, 4, 18, 2, 29, 39, 793615) + title="Test Title", created_by=self.user, modified_at=datetime(2022, 4, 18, 2, 29, 39, 793615) ) - url = f'/api/v2/dref/{dref.id}/' + url = f"/api/v2/dref/{dref.id}/" data = { - 'title': "New title", + "title": "New title", } # without `modified_at` @@ -881,13 +802,247 @@ def test_dref_latest_update(self): self.assertEqual(response.status_code, 400) # with `modified_at` less than instance `modified_at` - data['modified_at'] = datetime(2022, 2, 18, 2, 29, 39, 793615) + data["modified_at"] = datetime(2022, 2, 18, 2, 29, 39, 793615) self.client.force_authenticate(self.user) response = self.client.patch(url, data=data) self.assertEqual(response.status_code, 400) - data['modified_at'] = datetime.now() + data["modified_at"] = datetime.now() response = self.client.patch(url, data=data) self.assertEqual(response.status_code, 200) # Title should be latest since modified_at is greater than modified_at in database - self.assertEqual(response.data['title'], "New title") + self.assertEqual(response.data["title"], "New title") + + def test_dref_op_update_locking(self): + user1, _ = UserFactory.create_batch(2) + dref = DrefFactory.create( + title="Test Title", + created_by=user1, + is_published=True, + ) + dref.users.add(user1) + DrefOperationalUpdateFactory.create( + dref=dref, is_published=True, operational_update_number=1, modified_at=datetime.now() + ) + url = "/api/v2/dref-op-update/" + data = { + "dref": dref.id, + } + self.authenticate(user=user1) + response = self.client.post(url, data=data) + self.assert_201(response) + response_id = response.data["id"] + + # without `modified_at` + data = { + "title": "New Operation title", + "new_operational_end_date": "2022-10-10", + "reporting_timeframe": "2022-10-16", + "is_timeframe_extension_required": True, + } + url = f"/api/v2/dref-op-update/{response_id}/" + self.authenticate(user=user1) + response = self.client.patch(url, data) + self.assert_400(response) + + # with `modified_at` less than instance `modified_at` + data["modified_at"] = datetime.now() - timedelta(days=2) + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 400) + + def test_optimistic_lock_in_final_report(self): + user1 = UserFactory.create() + dref = DrefFactory.create( + title="Test Title", + created_by=user1, + is_published=True, + ) + final_report = DrefFinalReportFactory( + title="Test title", + created_by=user1, + dref=dref, + ) + url = f"/api/v2/dref-final-report/{final_report.id}/" + + # update data without `modified_at` + data = {"title": "New Updated Title"} + self.authenticate(user1) + response = self.client.patch(url, data) + self.assert_400(response) + + # with `modified_at` less than instance `modified_at` + data["modified_at"] = datetime.now() - timedelta(days=2) + response = self.client.patch(url, data=data) + self.assert_400(response) + + # with `modified_at` greater than instance `modified_at` + data["modified_at"] = datetime.now() + timedelta(days=2) + response = self.client.patch(url, data=data) + self.assert_200(response) + + def test_dref_permission(self): + user1 = UserFactory.create( + username="user1@test.com", + first_name="Test", + last_name="User1", + password="admin123", + email="user1@test.com", + is_superuser=True, + ) + user2 = UserFactory.create( + username="user2@test.com", + first_name="Test", + last_name="User2", + password="admin123", + email="user2@test.com", + ) + user3 = UserFactory.create( + username="user4@test.com", + first_name="Test", + last_name="User3", + password="admin123", + email="user4@test.com", + ) + user4 = UserFactory.create( + username="user3@test.com", + first_name="Test", + last_name="User4", + password="admin123", + email="user3@test.com", + ) + dref1 = DrefFactory.create( + title="Test Title", + created_by=user1, + ) + dref1.users.add(user2) + DrefFactory.create(title="Test Title New", created_by=user3) + get_url = "/api/v2/dref/" + # authenticate with superuser + # should be able to view all drefs + self.client.force_authenticate(user1) + response = self.client.get(get_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data["results"]), 2) + + # # let superuser patch the dref + patch_url = f"/api/v2/dref/{dref1.id}/" + data = {"title": "New test title", "modified_at": datetime.now()} + response = self.client.patch(patch_url, data=data) + self.assertEqual(response.status_code, 200) + + # # lets authenticate with user for whom dref is shared with + self.client.force_authenticate(user2) + response = self.client.get(get_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data["results"]), 1) + + # # try to patch by user + self.client.force_authenticate(user2) + data = { + "title": "New test title", + "modified_at": datetime.now() + timedelta(days=1), + } + response = self.client.patch(patch_url, data=data) + self.assertEqual(response.status_code, 200) + + # # try to authenticate with user who is neither assigned nor created_by + self.client.force_authenticate(user4) + response = self.client.get(get_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data["results"]), 0) + + def test_superuser_permisssion_operational_update(self): + super_user = UserFactory.create( + username="user1@test.com", + first_name="Test", + last_name="User1", + password="admin123", + email="user1@test.com", + is_superuser=True, + ) + dref = DrefFactory.create( + title="Test Title", + is_published=True, + ) + self.country1 = Country.objects.create(name="abc") + self.district1 = District.objects.create(name="test district1", country=self.country1) + url = "/api/v2/dref-op-update/" + data = { + "dref": dref.id, + "country": self.country1.id, + "district": [self.district1.id], + } + # authenticate with superuser + self.authenticate(super_user) + response = self.client.post(url, data=data) + self.assert_201(response) + + def test_operational_update_view_permission(self): + Dref.objects.all().delete() + user1, user2, user3 = UserFactory.create_batch(3) + dref = DrefFactory.create(title="Test Title", is_published=True, created_by=user1) + operational_update = DrefOperationalUpdateFactory.create( + dref=dref, + ) + operational_update.users.set([user3]) + self.country1 = Country.objects.create(name="abc") + self.district1 = District.objects.create(name="test district1", country=self.country1) + + # Here user1 is able to see the operational_update + url = "/api/v2/dref-op-update/" + self.authenticate(user1) + response = self.client.get(url) + self.assert_200(response) + + # authenticate with the user3 + # here user3 should be able to view dref + # since user3 is assigned to op-update created from dref + dref_url = "/api/v2/dref/" + self.authenticate(user3) + response = self.client.get(dref_url) + self.assert_200(response) + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], dref.id) + + # authenticate with user2 + # here user2 is not able to view dref + self.authenticate(user2) + response = self.client.get(dref_url) + self.assert_200(response) + self.assertEqual(len(response.data["results"]), 0) + + def test_concurrent_dref_operational_update(self): + user1, user2, user3 = UserFactory.create_batch(3) + dref = DrefFactory.create(title="Test Title", is_published=True, created_by=user1) + operational_update = DrefOperationalUpdateFactory.create(dref=dref, operational_update_number=1, created_by=user3) + operational_update.users.set([user3, user2]) + self.country1 = Country.objects.create(name="abc") + self.district1 = District.objects.create(name="test district1", country=self.country1) + + # Here user1 is able to see the operational_update + url = "/api/v2/dref-op-update/" + self.authenticate(user1) + response = self.client.get(url) + self.assert_200(response) + + # create another operational update from corresponding dref + operation_update2 = DrefOperationalUpdateFactory.create(dref=dref, operational_update_number=2) + operation_update2.users.set([user2]) + + # authenticate with user2 + url = "/api/v2/dref-op-update/" + self.authenticate(user2) + response = self.client.get(url) + self.assert_200(response) + + # user2 should also be able to view dref + dref_url = "/api/v2/dref/" + self.authenticate(user2) + response = self.client.get(dref_url) + self.assert_200(response) + self.assertEqual(len(response.data["results"]), 1) + + # authenticate with user1 + self.authenticate(user1) + response = self.client.get(dref_url) + self.assert_200(response) diff --git a/dref/translation.py b/dref/translation.py index 2f091a677..08b321d99 100644 --- a/dref/translation.py +++ b/dref/translation.py @@ -5,4 +5,4 @@ @register(Dref) class DrefTO(TranslationOptions): - fields = ('title',) + fields = ("title",) diff --git a/dref/utils.py b/dref/utils.py index 9b6d58ee6..eafc42c29 100644 --- a/dref/utils.py +++ b/dref/utils.py @@ -1,13 +1,59 @@ from django.conf import settings +from dref.models import ( + Dref, + DrefOperationalUpdate, + DrefFinalReport, +) +from django.db import models +from django.contrib.postgres.aggregates import ArrayAgg + def get_email_context(instance): from dref.serializers import DrefSerializer dref_data = DrefSerializer(instance).data email_context = { - 'id': dref_data['id'], - 'title': dref_data['title'], - 'frontend_url': settings.FRONTEND_URL, + "id": dref_data["id"], + "title": dref_data["title"], + "frontend_url": settings.FRONTEND_URL, } return email_context + + +def get_dref_users(): + dref_users_qs = Dref.objects.annotate( + created_user_list=ArrayAgg("created_by", filter=models.Q(created_by__isnull=False)), + users_list=ArrayAgg("users", filter=models.Q(users__isnull=False)), + op_users=models.Subquery( + DrefOperationalUpdate.objects.filter(dref=models.OuterRef("id")) + .order_by() + .values("dref") + .annotate(c=ArrayAgg("users", filter=models.Q(users__isnull=False))) + .values("c")[:1] + ), + fr_users=models.Subquery( + DrefFinalReport.objects.filter(dref=models.OuterRef("id")) + .order_by() + .values("dref") + .annotate(c=ArrayAgg("users", filter=models.Q(users__isnull=False))) + .values("c")[:1], + ), + ).values("id", "created_user_list", "users_list", "op_users", "fr_users") + dref_users_list = [] + for dref in dref_users_qs: + if dref["created_user_list"] is None: + dref["created_user_list"] = [] + if dref["users_list"] is None: + dref["users_list"] = [] + if dref["op_users"] is None: + dref["op_users"] = [] + if dref["fr_users"] is None: + dref["fr_users"] = [] + dref_users_list.append( + dict( + id=dref["id"], + users=set(list(dref["created_user_list"] + dref["users_list"] + dref["op_users"] + dref["fr_users"])), + ) + ) + return dref_users_list diff --git a/dref/views.py b/dref/views.py index 577dc2b93..97dd75852 100644 --- a/dref/views.py +++ b/dref/views.py @@ -1,4 +1,3 @@ -from django.db import models from django.contrib.auth.models import User from django.utils.translation import gettext from reversion.views import RevisionMixin @@ -28,11 +27,11 @@ DrefOperationalUpdateSerializer, DrefFinalReportSerializer, ) -from dref.filter_set import ( - DrefFilter, - DrefOperationalUpdateFilter +from dref.filter_set import DrefFilter, DrefOperationalUpdateFilter +from dref.permissions import ( + DrefOperationalUpdateUpdatePermission, + DrefFinalReportUpdatePermission, ) -from dref.permissions import DrefOperationalUpdateCreatePermission class DrefViewSet(RevisionMixin, viewsets.ModelViewSet): @@ -42,75 +41,79 @@ class DrefViewSet(RevisionMixin, viewsets.ModelViewSet): def get_queryset(self): user = self.request.user - queryset = Dref.objects.prefetch_related( - 'planned_interventions', - 'needs_identified', - 'national_society_actions', - 'users' - ).order_by('-created_at').distinct() + queryset = ( + Dref.objects.prefetch_related("planned_interventions", "needs_identified", "national_society_actions", "users") + .order_by("-created_at") + .distinct() + ) if user.is_superuser: return queryset else: - return queryset.filter(models.Q(created_by=user) | models.Q(users=user)) + return Dref.get_for(user) @action( detail=True, - url_path='publish', - methods=['post'], + url_path="publish", + methods=["post"], serializer_class=DrefSerializer, - permission_classes=[permissions.IsAuthenticated] + permission_classes=[permissions.IsAuthenticated], ) def get_published(self, request, pk=None, version=None): dref = self.get_object() if not dref.is_published: dref.is_published = True - dref.save(update_fields=['is_published']) - serializer = DrefSerializer(dref, context={'request': request}) + dref.save(update_fields=["is_published"]) + serializer = DrefSerializer(dref, context={"request": request}) return response.Response(serializer.data) class DrefOperationalUpdateViewSet(RevisionMixin, viewsets.ModelViewSet): serializer_class = DrefOperationalUpdateSerializer - permission_classes = [permissions.IsAuthenticated, DrefOperationalUpdateCreatePermission] + permission_classes = [permissions.IsAuthenticated] filterset_class = DrefOperationalUpdateFilter def get_queryset(self): user = self.request.user - queryset = DrefOperationalUpdate.objects.select_related( - 'national_society', - 'national_society', - 'disaster_type', - 'event_map', - 'cover_image', - 'budget_file', - 'assessment_report' - ).prefetch_related( - 'dref', - 'planned_interventions', - 'needs_identified', - 'national_society_actions', - 'users', - 'images', - 'photos', - ).order_by('-created_at').distinct() + queryset = ( + DrefOperationalUpdate.objects.select_related( + "national_society", + "national_society", + "disaster_type", + "event_map", + "cover_image", + "budget_file", + "assessment_report", + ) + .prefetch_related( + "dref", + "planned_interventions", + "needs_identified", + "national_society_actions", + "users", + "images", + "photos", + ) + .order_by("-created_at") + .distinct() + ) if user.is_superuser: return queryset else: - return queryset.filter(models.Q(created_by=user) | models.Q(users=user)) + return DrefOperationalUpdate.get_for(user) @action( detail=True, - url_path='publish', - methods=['post'], + url_path="publish", + methods=["post"], serializer_class=DrefOperationalUpdateSerializer, - permission_classes=[permissions.IsAuthenticated] + permission_classes=[permissions.IsAuthenticated], ) def get_published(self, request, pk=None, version=None): operational_update = self.get_object() if not operational_update.is_published: operational_update.is_published = True - operational_update.save(update_fields=['is_published']) - serializer = DrefOperationalUpdateSerializer(operational_update, context={'request': request}) + operational_update.save(update_fields=["is_published"]) + serializer = DrefOperationalUpdateSerializer(operational_update, context={"request": request}) return response.Response(serializer.data) @@ -119,31 +122,38 @@ class DrefFinalReportViewSet(RevisionMixin, viewsets.ModelViewSet): permission_classes = [permissions.IsAuthenticated] def get_queryset(self): - return DrefFinalReport.objects.prefetch_related( - 'dref__planned_interventions', - 'dref__needs_identified', - ).order_by('-created_at').distinct() + user = self.request.user + queryset = ( + DrefFinalReport.objects.prefetch_related( + "dref__planned_interventions", + "dref__needs_identified", + ) + .order_by("-created_at") + .distinct() + ) + if user.is_superuser: + return queryset + else: + return DrefFinalReport.get_for(user) @action( detail=True, - url_path='publish', - methods=['post'], + url_path="publish", + methods=["post"], serializer_class=DrefFinalReportSerializer, - permission_classes=[permissions.IsAuthenticated] + permission_classes=[permissions.IsAuthenticated], ) def get_published(self, request, pk=None, version=None): field_report = self.get_object() if field_report.is_published: - raise serializers.ValidationError( - gettext('Final Report %s is already published' % field_report) - ) + raise serializers.ValidationError(gettext("Final Report %s is already published" % field_report)) if not field_report.is_published: field_report.is_published = True - field_report.save(update_fields=['is_published']) + field_report.save(update_fields=["is_published"]) if not field_report.dref.is_final_report_created: field_report.dref.is_final_report_created = True - field_report.dref.save(update_fields=['is_final_report_created']) - serializer = DrefFinalReportSerializer(field_report, context={'request': request}) + field_report.dref.save(update_fields=["is_final_report_created"]) + serializer = DrefFinalReportSerializer(field_report, context={"request": request}) return response.Response(serializer.data) @@ -151,64 +161,36 @@ class DrefOptionsView(views.APIView): """ Options for various attribute related to Dref """ + permission_classes = [permissions.IsAuthenticated] def get(self, request, version=None): options = { - 'status': [ - { - 'key': status.value, - 'value': status.label - } for status in Dref.Status + "status": [{"key": status.value, "value": status.label} for status in Dref.Status], + "type_of_onset": [{"key": onset.value, "value": onset.label} for onset in Dref.OnsetType], + "disaster_category": [{"key": disaster.value, "value": disaster.label} for disaster in Dref.DisasterCategory], + "planned_interventions": [ + {"key": intervention[0], "value": intervention[1]} for intervention in PlannedIntervention.Title.choices ], - 'type_of_onset': [ - { - 'key': onset.value, - 'value': onset.label - } for onset in Dref.OnsetType - ], - 'disaster_category': [ - { - 'key': disaster.value, - 'value': disaster.label - } for disaster in Dref.DisasterCategory + "needs_identified": [{"key": need[0], "value": need[1]} for need in IdentifiedNeed.Title.choices], + "national_society_actions": [ + {"key": action[0], "value": action[1]} for action in NationalSocietyAction.Title.choices ], - 'planned_interventions': [ + "users": [ { - 'key': intervention[0], - 'value': intervention[1] - } for intervention in PlannedIntervention.Title.choices + "id": user.id, + "email": user.email, + "username": user.username, + "first_name": user.first_name, + "last_name": user.last_name, + } + for user in User.objects.filter(is_active=True) ], - 'needs_identified': [ - { - 'key': need[0], - 'value': need[1] - } for need in IdentifiedNeed.Title.choices - ], - 'national_society_actions': [ - { - 'key': action[0], - 'value': action[1] - } for action in NationalSocietyAction.Title.choices - ], - 'users': [ - { - 'id': user.id, - 'email': user.email, - 'username': user.username, - 'first_name': user.first_name, - 'last_name': user.last_name - } for user in User.objects.all() - ] } return response.Response(options) -class DrefFileViewSet( - mixins.ListModelMixin, - mixins.CreateModelMixin, - viewsets.GenericViewSet -): +class DrefFileViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet): permission_class = [permissions.IsAuthenticated] serializer_class = DrefFileSerializer @@ -219,15 +201,15 @@ def get_queryset(self): @action( detail=False, - url_path='multiple', - methods=['POST'], + url_path="multiple", + methods=["POST"], permission_classes=[permissions.IsAuthenticated], ) def multiple_file(self, request, pk=None, version=None): # converts querydict to original dict - files = dict((request.data).lists())['file'] - data = [{'file': file} for file in files] - file_serializer = DrefFileSerializer(data=data, context={'request': request}, many=True) + files = dict((request.data).lists())["file"] + data = [{"file": file} for file in files] + file_serializer = DrefFileSerializer(data=data, context={"request": request}, many=True) if file_serializer.is_valid(): file_serializer.save() return response.Response(file_serializer.data, status=status.HTTP_201_CREATED) diff --git a/eap/models.py b/eap/models.py index d290433d6..7b618cbe9 100644 --- a/eap/models.py +++ b/eap/models.py @@ -1,10 +1,6 @@ from django.db import models from django.conf import settings from django.utils.translation import gettext_lazy as _ - -from main.enums import TextChoices -from deployments.models import Sectors -from main.enums import IntegerChoices from api.models import ( Country, District, @@ -13,7 +9,7 @@ class EarlyActionIndicator(models.Model): - class IndicatorChoices(TextChoices): # TODO these indicator are yet to be provided by client. + class IndicatorChoices(models.TextChoices): # TODO these indicator are yet to be provided by client. INDICATOR_1 = 'indicator_1', _('Indicator 1') INDICATOR_2 = 'indicator_2', _('Indicator 2') @@ -32,7 +28,7 @@ def __str__(self): class EarlyAction(models.Model): - class Sector(IntegerChoices): + class Sector(models.IntegerChoices): SHELTER_HOUSING_AND_SETTLEMENTS = 0, _('Shelter, Housing And Settlements') LIVELIHOODS = 1, _('Livelihoods') MULTI_PURPOSE_CASH = 2, _('Multi-purpose Cash') @@ -67,7 +63,7 @@ def __str__(self): class EAP(models.Model): - class Status(TextChoices): # TODO some more status choices are to be expected by client. + class Status(models.TextChoices): # TODO some more status choices are to be expected by client. APPROVED = 'approved', _('Approved') IN_PROCESS = 'in_process', _('In Process') diff --git a/flash_update/models.py b/flash_update/models.py index 4de36a7fa..d07875622 100644 --- a/flash_update/models.py +++ b/flash_update/models.py @@ -16,8 +16,6 @@ ActionCategory, ) -from main.enums import TextChoices - @reversion.register() class FlashGraphicMap(models.Model): diff --git a/flash_update/search_indexes.py b/flash_update/search_indexes.py new file mode 100644 index 000000000..855bb8dcb --- /dev/null +++ b/flash_update/search_indexes.py @@ -0,0 +1,15 @@ +from haystack import indexes + +from flash_update.models import FlashUpdate + + +class FlashUpdateIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.EdgeNgramField(document=True, use_template=True) + name = indexes.EdgeNgramField(model_attr='title') + created_at = indexes.DateTimeField(model_attr='created_at') + + def get_model(self): + return FlashUpdate + + def index_queryset(self, using=None): + return self.get_model().objects.all() diff --git a/flash_update/templates/search/indexes/flash_update/flashupdate_text.txt b/flash_update/templates/search/indexes/flash_update/flashupdate_text.txt new file mode 100644 index 000000000..ef664e650 --- /dev/null +++ b/flash_update/templates/search/indexes/flash_update/flashupdate_text.txt @@ -0,0 +1 @@ +{{object.name}} \ No newline at end of file diff --git a/local_units/__init__.py b/local_units/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/local_units/admin.py b/local_units/admin.py new file mode 100644 index 000000000..31feef229 --- /dev/null +++ b/local_units/admin.py @@ -0,0 +1,6 @@ +from django.contrib.gis import admin + +from .models import LocalUnit, LocalUnitType + +admin.site.register(LocalUnit, admin.OSMGeoAdmin) +admin.site.register(LocalUnitType, admin.ModelAdmin) diff --git a/local_units/apps.py b/local_units/apps.py new file mode 100644 index 000000000..3307515af --- /dev/null +++ b/local_units/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class LocalUnits(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'local_units' diff --git a/local_units/management/commands/import-local-units-csv.py b/local_units/management/commands/import-local-units-csv.py new file mode 100644 index 000000000..9326c276c --- /dev/null +++ b/local_units/management/commands/import-local-units-csv.py @@ -0,0 +1,50 @@ +from django.db import transaction +import csv +from django.core.management.base import BaseCommand, CommandError +from django.contrib.gis.geos import Point + +from api.models import Country +from ...models import LocalUnit, LocalUnitType + + +class Command(BaseCommand): + help = "Import LocalUnits data from CSV" + missing_args_message = "Filename is missing. Filename / path to CSV file required. Required headers in CSV: distr_code, GOadm2code, ADM2" + + def add_arguments(self, parser): + parser.add_argument('filename', nargs='+', type=str) + + @transaction.atomic + def handle(self, *args, **options): + filename = options['filename'][0] + with open(filename) as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + unit = LocalUnit() + unit.country = Country.objects.get(iso3=row['ISO3']) + unit.type, created = LocalUnitType.objects.get_or_create( + level=row['TYPECODE'], + name=row['TYPENAME'] + ) + if created: + print(f'New LocalUnitType created: {unit.type.name}') + + unit.local_branch_name = row['NAME_LOC'] + unit.english_branch_name = row['NAME_EN'] + # TODO: why is it here? No such column in this table: unit.branch_level = int(row['TYPECODE']) + unit.postcode = int(row['POSTCODE']) if row['POSTCODE'] else None + unit.address_loc = row['ADDRESS_LOC'] + unit.address_en = row['ADDRESS_EN'] + unit.city_loc = row['CITY_LOC'] + unit.city_en = row['CITY_EN'] + unit.focal_person_loc = row['FOCAL_PERSON_LOC'] + unit.focal_person_en = row['FOCAL_PERSON_EN'] + unit.phone = row['TELEPHONE'] + unit.email = row['EMAIL'] + unit.link = row['WEBSITE'] + unit.source_en = row['SOURCE_EN'] + unit.source_loc = row['SOURCE_LOC'] + unit.location = Point(float(row['LONGITUDE']), float(row['LATITUDE'])) + unit.save() + name = unit.local_branch_name if unit.local_branch_name else unit.english_branch_name + print(f'{name} saved') diff --git a/local_units/migrations/0001_initial.py b/local_units/migrations/0001_initial.py new file mode 100644 index 000000000..544f8cb3d --- /dev/null +++ b/local_units/migrations/0001_initial.py @@ -0,0 +1,53 @@ +# Generated by Django 3.2.16 on 2023-01-10 23:01 + +import django.contrib.gis.db.models.fields +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('api', '0160_merge_0159_auto_20221022_1542_0159_auto_20221028_0940'), + ] + + operations = [ + migrations.CreateModel( + name='LocalUnitType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('level', models.IntegerField(validators=[django.core.validators.MaxValueValidator(10), django.core.validators.MinValueValidator(0)], verbose_name='Level')), + ('name', models.CharField(max_length=100, verbose_name='Name')), + ], + ), + migrations.CreateModel( + name='LocalUnit', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('local_branch_name', models.CharField(max_length=255, verbose_name='Branch name in local language')), + ('english_branch_name', models.CharField(max_length=255, verbose_name='Branch name in English')), + ('created_at', models.DateTimeField(auto_now=True, verbose_name='Created at')), + ('modified_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('draft', models.BooleanField(default=False, verbose_name='Draft')), + ('validated', models.BooleanField(default=True, verbose_name='Validated')), + ('source_en', models.CharField(blank=True, max_length=500, null=True, verbose_name='Source in Local Language')), + ('source_loc', models.CharField(blank=True, max_length=500, null=True, verbose_name='Source in English')), + ('address_loc', models.CharField(blank=True, max_length=500, null=True, verbose_name='Address in local language')), + ('address_en', models.CharField(blank=True, max_length=500, null=True, verbose_name='Address in English')), + ('city_loc', models.CharField(max_length=255, verbose_name='City in local language')), + ('city_en', models.CharField(max_length=255, verbose_name='City in English')), + ('focal_person_loc', models.CharField(blank=True, max_length=255, null=True, verbose_name='Focal person for local language')), + ('focal_person_en', models.CharField(blank=True, max_length=255, null=True, verbose_name='Focal person for English')), + ('postcode', models.CharField(max_length=10, null=True, verbose_name='Postal code')), + ('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Telephone')), + ('email', models.EmailField(blank=True, max_length=255, null=True, verbose_name='Email')), + ('link', models.URLField(blank=True, max_length=255, null=True, verbose_name='Social link')), + ('location', django.contrib.gis.db.models.fields.PointField(srid=4326)), + ('country', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='local_unit_country', to='api.country', verbose_name='Country')), + ('type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='local_unit_type', to='local_units.localunittype', verbose_name='Type')), + ], + ), + ] diff --git a/local_units/migrations/0002_alter_localunit_phone.py b/local_units/migrations/0002_alter_localunit_phone.py new file mode 100644 index 000000000..b0ff71154 --- /dev/null +++ b/local_units/migrations/0002_alter_localunit_phone.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.17 on 2023-02-15 14:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('local_units', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='localunit', + name='phone', + field=models.CharField(blank=True, max_length=30, null=True, verbose_name='Telephone'), + ), + ] diff --git a/local_units/migrations/0003_alter_localunit_validated.py b/local_units/migrations/0003_alter_localunit_validated.py new file mode 100644 index 000000000..38404c90b --- /dev/null +++ b/local_units/migrations/0003_alter_localunit_validated.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.17 on 2023-02-16 11:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('local_units', '0002_alter_localunit_phone'), + ] + + operations = [ + migrations.AlterField( + model_name='localunit', + name='validated', + field=models.BooleanField(default=False, verbose_name='Validated'), + ), + ] diff --git a/local_units/migrations/__init__.py b/local_units/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/local_units/models.py b/local_units/models.py new file mode 100644 index 000000000..0db7358f9 --- /dev/null +++ b/local_units/models.py @@ -0,0 +1,115 @@ +from django.contrib.gis.db import models +from django.core.validators import MaxValueValidator, MinValueValidator +from django.utils.translation import gettext_lazy as _ + +from api.models import Country + + +class LocalUnitType(models.Model): + level = models.IntegerField( + verbose_name=_('Level'), + validators=[ + MaxValueValidator(10), + MinValueValidator(0) + ] + ) + name = models.CharField( + max_length=100, + verbose_name=_('Name') + ) + + def __str__(self): + return f'{self.name} ({self.level})' + + +class LocalUnit(models.Model): + country = models.ForeignKey( + Country, on_delete=models.SET_NULL, verbose_name=_('Country'), + related_name='local_unit_country', null=True + ) + type = models.ForeignKey( + LocalUnitType, on_delete=models.SET_NULL, verbose_name=_('Type'), + related_name='local_unit_type', null=True + ) + local_branch_name = models.CharField( + max_length=255, + verbose_name=_('Branch name in local language') + ) + english_branch_name = models.CharField( + max_length=255, + verbose_name=_('Branch name in English') + ) + + created_at = models.DateTimeField( + verbose_name=_('Created at'), + auto_now=True + ) + modified_at = models.DateTimeField( + verbose_name=_('Updated at'), + auto_now=True + ) + draft = models.BooleanField(default=False, verbose_name=_('Draft')) + validated = models.BooleanField(default=False, verbose_name=_('Validated')) + source_en = models.CharField( + max_length=500, + blank=True, + null=True, + verbose_name=_('Source in Local Language') + ) + source_loc = models.CharField( + max_length=500, + blank=True, + null=True, + verbose_name=_('Source in English') + ) + address_loc = models.CharField( + max_length=500, + blank=True, + null=True, + verbose_name=_('Address in local language') + ) + address_en = models.CharField( + max_length=500, + blank=True, + null=True, + verbose_name=_('Address in English') + ) + city_loc = models.CharField(max_length=255, verbose_name=_('City in local language')) + city_en = models.CharField(max_length=255, verbose_name=_('City in English')) + focal_person_loc = models.CharField( + max_length=255, + blank=True, + null=True, + verbose_name=_('Focal person for local language') + ) + focal_person_en = models.CharField( + max_length=255, + blank=True, + null=True, + verbose_name=_('Focal person for English') + ) + postcode = models.CharField(max_length=10, null=True, verbose_name=_('Postal code')) + phone = models.CharField( + max_length=30, + blank=True, + null=True, + verbose_name=_('Telephone') + ) + email = models.EmailField( + max_length=255, + blank=True, + null=True, + verbose_name=_('Email') + ) + link = models.URLField( + max_length=255, + blank=True, + null=True, + verbose_name=_('Social link') + ) + location = models.PointField() + + def __str__(self): + branch_name = self.local_branch_name or self.english_branch_name + return f'{branch_name} ({self.country.name})' + \ No newline at end of file diff --git a/local_units/serializers.py b/local_units/serializers.py new file mode 100644 index 000000000..a222b8cb4 --- /dev/null +++ b/local_units/serializers.py @@ -0,0 +1,46 @@ +import json +from rest_framework.serializers import ModelSerializer, SerializerMethodField + +from .models import LocalUnit, LocalUnitType +from api.models import Country + + +class CountrySerializer(ModelSerializer): + + class Meta: + model = Country + fields = ( + 'name', 'iso3' + ) + +class LocalUnitTypeSerializer(ModelSerializer): + + class Meta: + model = LocalUnitType + fields = ( + 'name', 'level' + ) + +class LocalUnitSerializer(ModelSerializer): + location = SerializerMethodField() + country = CountrySerializer() + type = LocalUnitTypeSerializer() + class Meta: + model = LocalUnit + fields = [ + 'local_branch_name', 'english_branch_name', 'type', 'country', + 'created_at', 'modified_at', 'draft', 'validated', 'postcode', + 'address_loc', 'address_en', 'city_loc', 'city_en', 'link', + 'location', 'focal_person_loc', 'focal_person_en', + 'source_loc', 'source_en' + # 'email', 'phone', + ] + + def get_location(self, unit): + return json.loads(unit.location.geojson) + + def get_country(self, unit): + return {'country'} + + def get_type(self, unit): + return {'type'} diff --git a/local_units/test_views.py b/local_units/test_views.py new file mode 100644 index 000000000..afd68d491 --- /dev/null +++ b/local_units/test_views.py @@ -0,0 +1,109 @@ +import factory +from django.test import TestCase +from django.contrib.gis.geos import Point + +from .models import LocalUnit, LocalUnitType +from api.models import Country, Region + + +class LocalUnitFactory(factory.django.DjangoModelFactory): + class Meta: + model = LocalUnit + + location = Point(12, 38) + + +class TestLocalUnitsListView(TestCase): + def setUp(self): + region = Region.objects.create(name=2) + country = Country.objects.create(name='Nepal', iso3='NLP', iso='NP', region=region) + country_1 = Country.objects.create(name='Philippines', iso3='PHL', iso='PH', region=region) + type = LocalUnitType.objects.create(level=0, name='Level 0') + type_1 = LocalUnitType.objects.create(level=1, name='Level 1') + LocalUnitFactory.create_batch(5, country=country, type=type, draft=True, validated=False) + LocalUnitFactory.create_batch(5, country=country_1, type=type_1, draft=False, validated=True) + + def test_list(self): + response = self.client.get('/api/v2/local-unit/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], 10) + self.assertEqual(response.data['results'][0]['location']['coordinates'], [12, 38]) + self.assertEqual(response.data['results'][0]['country']['name'], 'Nepal') + self.assertEqual(response.data['results'][0]['country']['iso3'], 'NLP') + self.assertEqual(response.data['results'][0]['type']['name'], 'Level 0') + self.assertEqual(response.data['results'][0]['type']['level'], 0) + + def test_filter(self): + response = self.client.get('/api/v2/local-unit/?country__name=Nepal') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], 5) + + response = self.client.get('/api/v2/local-unit/?country__name=Philippines') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], 5) + + response = self.client.get('/api/v2/local-unit/?country__name=Belgium') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], 0) + + response = self.client.get('/api/v2/local-unit/?country__iso=BE') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], 0) + + response = self.client.get('/api/v2/local-unit/?country__iso3=BEL') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], 0) + + response = self.client.get('/api/v2/local-unit/?country__iso=BE') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], 0) + + response = self.client.get('/api/v2/local-unit/?country__iso3=PHL') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], 5) + + response = self.client.get('/api/v2/local-unit/?country__iso=NP') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], 5) + + response = self.client.get('/api/v2/local-unit/?type__level=0') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], 5) + + response = self.client.get('/api/v2/local-unit/?type__level=4') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], 0) + + response = self.client.get('/api/v2/local-unit/?draft=true') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], 5) + + response = self.client.get('/api/v2/local-unit/?draft=false') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], 5) + + response = self.client.get('/api/v2/local-unit/?validated=true') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], 5) + + response = self.client.get('/api/v2/local-unit/?validated=false') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], 5) + + +class TestLocalUnitsDetailView(TestCase): + def setUp(self): + region = Region.objects.create(name=2) + country = Country.objects.create(name='Nepal', iso3='NLP', region=region) + type = LocalUnitType.objects.create(level=0, name='Level 0') + LocalUnitFactory.create_batch(2, country=country, type=type) + + def test_detail(self): + local_unit = LocalUnit.objects.all().first() + response = self.client.get(f'/api/v2/local-unit/{local_unit.id}/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['location']['coordinates'], [12, 38]) + self.assertEqual(response.data['country']['name'], 'Nepal') + self.assertEqual(response.data['country']['iso3'], 'NLP') + self.assertEqual(response.data['type']['name'], 'Level 0') + self.assertEqual(response.data['type']['level'], 0) diff --git a/local_units/views.py b/local_units/views.py new file mode 100644 index 000000000..c004f5b2f --- /dev/null +++ b/local_units/views.py @@ -0,0 +1,32 @@ +from rest_framework.generics import ( + ListAPIView, RetrieveAPIView +) +from django_filters import rest_framework as filters + +from .models import LocalUnit +from .serializers import LocalUnitSerializer + + +class LocalUnitFilters(filters.FilterSet): + class Meta: + model = LocalUnit + fields = ( + 'country__name', + 'country__iso3', + 'country__iso', + 'type__level', + 'draft', + 'validated', + ) + + +class LocalUnitListAPIView(ListAPIView): + queryset = LocalUnit.objects.all() + serializer_class = LocalUnitSerializer + filterset_class = LocalUnitFilters + search_fields = ('local_branch_name', 'english_branch_name',) + + +class LocalUnitDetailAPIView(RetrieveAPIView): + queryset = LocalUnit.objects.all() + serializer_class = LocalUnitSerializer diff --git a/main/__init__.py b/main/__init__.py index 25b772a4d..65e092f3c 100644 --- a/main/__init__.py +++ b/main/__init__.py @@ -3,4 +3,4 @@ from .celery import app as celery_app __all__ = ['celery_app'] -__version__ = '1.1.460' +__version__ = '1.1.471' diff --git a/main/celery.py b/main/celery.py index c3df954ff..fa710210a 100644 --- a/main/celery.py +++ b/main/celery.py @@ -1,9 +1,17 @@ import os import celery +from django.conf import settings +from main import sentry + class CustomCeleryApp(celery.Celery): - pass + def on_configure(self): + if settings.SENTRY_DSN: + sentry.init_sentry( + app_type='WORKER', + **settings.SENTRY_CONFIG, + ) # set the default Django settings module for the 'celery' program. diff --git a/main/exception_handler.py b/main/exception_handler.py new file mode 100644 index 000000000..c0ae52e28 --- /dev/null +++ b/main/exception_handler.py @@ -0,0 +1,36 @@ +import sentry_sdk + +from rest_framework.views import exception_handler +from rest_framework.response import Response +from rest_framework import status + + +standard_error_string = ( + 'Something unexpected has occured. ' + 'Please contact an admin to fix this issue.' +) + + +def custom_exception_handler(exc, context): + # Default exception handler + response = exception_handler(exc, context) + + # For 500 errors, we create new response and add extra attributes to sentry + if not response: + request = context.get('request') + if request and request.user and request.user.id: + with sentry_sdk.configure_scope() as scope: + scope.user = { + 'id': request.user.id, + 'email': request.user.email, + } + scope.set_extra('is_superuser', request.user.is_superuser) + sentry_sdk.capture_exception() + response_data = { + 'errors': { + 'non_field_errors': [standard_error_string] + }, + } + response = Response(response_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + return response diff --git a/main/frontend.py b/main/frontend.py index 1b0aaadcb..d4d44dba0 100644 --- a/main/frontend.py +++ b/main/frontend.py @@ -2,7 +2,7 @@ def get_project_url(id): - return f'https://{settings.FRONTEND_URL}/three-w/{id}/' + return f'{settings.FRONTEND_URL}/three-w/{id}/' def get_flash_update_url(id): diff --git a/main/runserver.sh b/main/runserver.sh index c02ed064f..b14379d87 100755 --- a/main/runserver.sh +++ b/main/runserver.sh @@ -39,21 +39,22 @@ exec gunicorn main.wsgi:application \ # Exporting env vars, like: echo "export PRODUCTION=\"$PRODUCTION\"" >> $HOME/.env printenv | sed 's/^\([a-zA-Z0-9_]*\)=\(.*\)$/export \1="\2"/g' > $HOME/.env -# # XXX: Changing cron schedule expressions may break or send multiple notifications. -# (crontab -l 2>/dev/null; echo 'SHELL=/bin/bash') | crontab - -# (crontab -l 2>/dev/null; echo '15 * * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py ingest_appeal_docs >> /home/ifrc/logs/ingest_appeal_docs.log 2>&1') | crontab - -# #(crontab -l 2>/dev/null; echo '30 * * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py ingest_mdb >> /home/ifrc/logs/ingest_mdb.log 2>&1') | crontab - -# (crontab -l 2>/dev/null; echo '45 * * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py ingest_appeals >> /home/ifrc/logs/ingest_appeals.log 2>&1') | crontab - -# (crontab -l 2>/dev/null; echo '51 * * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py revoke_staff_status >> /home/ifrc/logs/revoke_staff_status.log 2>&1') | crontab - -# #(crontab -l 2>/dev/null; echo '*/20 * * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py ingest_gdacs >> /home/ifrc/logs/ingest_gdacs.log 2>&1') | crontab - -# #(crontab -l 2>/dev/null; echo '0 2 * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py ingest_who >> /home/ifrc/logs/ingest_who.log 2>&1') | crontab - -# (crontab -l 2>/dev/null; echo '*/5 * * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py index_and_notify >> /home/ifrc/logs/index_and_notify.log 2>&1') | crontab - -# #(crontab -l 2>/dev/null; echo '10 2 * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py scrape_pdfs >> /home/ifrc/logs/scrape_pdfs.log 2>&1') | crontab - -# (crontab -l 2>/dev/null; echo '30 1 * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py ingest_databank >> /home/ifrc/logs/ingest_databank.log 2>&1') | crontab - -# (crontab -l 2>/dev/null; echo '*/10 * * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py sync_molnix >> /home/ifrc/logs/sync_molnix.log 2>&1') | crontab - -# (crontab -l 2>/dev/null; echo '0 3 * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py update_project_status >> /home/ifrc/logs/update_project_status.log 2>&1') | crontab - -# (crontab -l 2>/dev/null; echo '0 9 * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py user_registration_reminder >> /home/ifrc/logs/user_registration_reminder.log 2>&1') | crontab - -# service cron start +# XXX: Changing cron schedule expressions may break or send multiple notifications. +(crontab -l 2>/dev/null; echo 'SHELL=/bin/bash') | crontab - +(crontab -l 2>/dev/null; echo '15 * * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py ingest_appeal_docs >> /home/ifrc/logs/ingest_appeal_docs.log 2>&1') | crontab - +#(crontab -l 2>/dev/null; echo '30 * * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py ingest_mdb >> /home/ifrc/logs/ingest_mdb.log 2>&1') | crontab - +(crontab -l 2>/dev/null; echo '45 * * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py ingest_appeals >> /home/ifrc/logs/ingest_appeals.log 2>&1') | crontab - +(crontab -l 2>/dev/null; echo '51 * * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py revoke_staff_status >> /home/ifrc/logs/revoke_staff_status.log 2>&1') | crontab - +#(crontab -l 2>/dev/null; echo '*/20 * * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py ingest_gdacs >> /home/ifrc/logs/ingest_gdacs.log 2>&1') | crontab - +#(crontab -l 2>/dev/null; echo '0 2 * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py ingest_who >> /home/ifrc/logs/ingest_who.log 2>&1') | crontab - +(crontab -l 2>/dev/null; echo '*/5 * * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py index_and_notify >> /home/ifrc/logs/index_and_notify.log 2>&1') | crontab - +#(crontab -l 2>/dev/null; echo '10 2 * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py scrape_pdfs >> /home/ifrc/logs/scrape_pdfs.log 2>&1') | crontab - +(crontab -l 2>/dev/null; echo '30 1 * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py ingest_databank >> /home/ifrc/logs/ingest_databank.log 2>&1') | crontab - +(crontab -l 2>/dev/null; echo '*/10 * * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py sync_molnix >> /home/ifrc/logs/sync_molnix.log 2>&1') | crontab - +(crontab -l 2>/dev/null; echo '0 3 * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py update_project_status >> /home/ifrc/logs/update_project_status.log 2>&1') | crontab - +(crontab -l 2>/dev/null; echo '0 9 * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py user_registration_reminder >> /home/ifrc/logs/user_registration_reminder.log 2>&1') | crontab - +(crontab -l 2>/dev/null; echo '0 0 * * * . /home/ifrc/.env; python /home/ifrc/go-api/manage.py ingest_country_plan_file >> /home/ifrc/logs/ingest_country_plan_file.log 2>&1') | crontab - +service cron start tail -n 0 -f $HOME/logs/*.log & diff --git a/main/sentry.py b/main/sentry.py new file mode 100644 index 000000000..dd4da8ded --- /dev/null +++ b/main/sentry.py @@ -0,0 +1,96 @@ +import os + +import sentry_sdk +from django.core.exceptions import PermissionDenied +from celery.exceptions import Retry as CeleryRetry +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.integrations.celery import CeleryIntegration +from sentry_sdk.integrations.django import DjangoIntegration +from sentry_sdk.integrations.redis import RedisIntegration + +# Celery Terminated Exception: The worker processing a job has been terminated by user request. +from billiard.exceptions import Terminated + +IGNORED_ERRORS = [ + Terminated, + PermissionDenied, + CeleryRetry, +] +IGNORED_LOGGERS = [ + 'django.core.exceptions.ObjectDoesNotExist', +] + +for _logger in IGNORED_LOGGERS: + ignore_logger(_logger) + + +class InvalidGitRepository(Exception): + pass + + +def fetch_git_sha(path, head=None): + """ + Source: https://github.com/getsentry/raven-python/blob/03559bb05fd963e2be96372ae89fb0bce751d26d/raven/versioning.py + >>> fetch_git_sha(os.path.dirname(__file__)) + """ + if not head: + head_path = os.path.join(path, '.git', 'HEAD') + if not os.path.exists(head_path): + raise InvalidGitRepository( + 'Cannot identify HEAD for git repository at %s' % (path,)) + + with open(head_path, 'r') as fp: + head = str(fp.read()).strip() + + if head.startswith('ref: '): + head = head[5:] + revision_file = os.path.join( + path, '.git', *head.split('/') + ) + else: + return head + else: + revision_file = os.path.join(path, '.git', 'refs', 'heads', head) + + if not os.path.exists(revision_file): + if not os.path.exists(os.path.join(path, '.git')): + raise InvalidGitRepository( + '%s does not seem to be the root of a git repository' % (path,)) + + # Check for our .git/packed-refs' file since a `git gc` may have run + # https://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery + packed_file = os.path.join(path, '.git', 'packed-refs') + if os.path.exists(packed_file): + with open(packed_file) as fh: + for line in fh: + line = line.rstrip() + if line and line[:1] not in ('#', '^'): + try: + revision, ref = line.split(' ', 1) + except ValueError: + continue + if ref == head: + return str(revision) + + raise InvalidGitRepository( + 'Unable to find ref to head "%s" in repository' % (head,)) + + with open(revision_file) as fh: + return str(fh.read()).strip() + + +def init_sentry(app_type, tags={}, **config): + integrations = [ + CeleryIntegration(), + DjangoIntegration(), + RedisIntegration(), + ] + sentry_sdk.init( + **config, + ignore_errors=IGNORED_ERRORS, + integrations=integrations, + ) + with sentry_sdk.configure_scope() as scope: + scope.set_tag('app_type', app_type) + for tag, value in tags.items(): + scope.set_tag(tag, value) diff --git a/main/settings.py b/main/settings.py index 43d3e146e..3f06511a6 100644 --- a/main/settings.py +++ b/main/settings.py @@ -6,10 +6,13 @@ from django.utils.translation import gettext_lazy as _ # from celery.schedules import crontab -from requests.packages.urllib3.util.retry import Retry +from urllib3.util.retry import Retry +from corsheaders.defaults import default_headers + +from main import sentry BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -DEFAULT_AUTO_FIELD='django.db.models.AutoField' +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' env = environ.Env( # Django @@ -20,7 +23,7 @@ DJANGO_MEDIA_ROOT=(str, os.path.join(BASE_DIR, 'media')), DJANGO_STATIC_URL=(str, '/static/'), DJANGO_STATIC_ROOT=(str, os.path.join(BASE_DIR, 'static')), - DJANGO_ADDITIONAL_ALLOWED_HOSTS=(list, []), # Eg: api.go.ifrc.org,goadmin.ifrc.org,prddsgocdnapi.azureedge.net + DJANGO_ADDITIONAL_ALLOWED_HOSTS=(list, []), # Eg: api.go.ifrc.org, goadmin.ifrc.org, dsgocdnapi.azureedge.net GO_ENVIRONMENT=(str, 'development'), # staging, production # API_FQDN=str, # sub-domain.domain.domain-extension @@ -40,8 +43,8 @@ EMAIL_PORT=(str, None), EMAIL_USER=(str, None), EMAIL_PASS=(str, None), - TEST_EMAILS=(list, ['im@ifrc.org']), DEBUG_EMAIL=(bool, False), # This was 0/1 before + # TEST_EMAILS=(list, ['im@ifrc.org']), # maybe later # AWS Translate NOTE: not used right now AWS_TRANSLATE_ACCESS_KEY=(str, None), AWS_TRANSLATE_SECRET_KEY=(str, None), @@ -69,6 +72,12 @@ GO_FTPUSER=(str, None), GO_FTPPASS=(str, None), GO_DBPASS=(str, None), + # Appeal Server Credentials (https://go-api.ifrc.org/api/) + APPEALS_USER=(str, None), + APPEALS_PASS=(str, None), + # Sentry + SENTRY_DSN=(str, None), + SENTRY_SAMPLE_RATE=(float, 0.2), ) @@ -144,11 +153,14 @@ 'dref', 'flash_update', 'eap', + 'country_plan', + 'local_units', # Utils Apps 'tinymce', 'admin_auto_filters', # 'django_celery_beat', + 'haystack', # Logging 'reversion', @@ -167,6 +179,7 @@ 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.SessionAuthentication', ), + 'EXCEPTION_HANDLER': 'main.exception_handler.custom_exception_handler', 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 'PAGE_SIZE': 50, 'DEFAULT_FILTER_BACKENDS': ( @@ -192,6 +205,7 @@ 'django.contrib.sessions.middleware.SessionMiddleware', 'corsheaders.middleware.CorsMiddleware', 'django.middleware.locale.LocaleMiddleware', + # 'middlewares.middlewares.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -208,6 +222,9 @@ ) CORS_ORIGIN_ALLOW_ALL = True +CORS_ALLOW_HEADERS = list(default_headers) + [ + 'sentry-trace', +] ROOT_URLCONF = 'main.urls' @@ -246,10 +263,10 @@ } AUTH_PASSWORD_VALIDATORS = [ - {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, - {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, - {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, - {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, + {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, + {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, + {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, + {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, {'NAME': 'main.validators.NumberValidator', }, {'NAME': 'main.validators.UppercaseValidator', }, {'NAME': 'main.validators.LowercaseValidator', }, @@ -355,8 +372,8 @@ EMAIL_PORT = env('EMAIL_PORT') EMAIL_USER = env('EMAIL_USER') EMAIL_PASS = env('EMAIL_PASS') -TEST_EMAILS = env('TEST_EMAILS') DEBUG_EMAIL = env('DEBUG_EMAIL') +# TEST_EMAILS = env('TEST_EMAILS') # maybe later DATA_UPLOAD_MAX_MEMORY_SIZE = 104857600 # default 2621440, 2.5MB -> 100MB # default 1000, was not enough for Mozambique Cyclone Idai data @@ -466,3 +483,44 @@ # MISC FRONTEND_URL = env('FRONTEND_URL') + + +DREF_OP_UPDATE_FINAL_REPORT_UPDATE_ERROR_MESSAGE = "OBSOLETE_PAYLOAD" + +# Appeal Server Credentials +APPEALS_USER = env('APPEALS_USER') +APPEALS_PASS = env('APPEALS_PASS') + +# Handmade Git Command +LAST_GIT_TAG = max(os.listdir(os.path.join(BASE_DIR, '.git', 'refs', 'tags')), default=0) + +# Sentry Config +SENTRY_DSN = env('SENTRY_DSN') +SENTRY_SAMPLE_RATE = env('SENTRY_SAMPLE_RATE') + +SENTRY_CONFIG = { + 'dsn': SENTRY_DSN, + 'send_default_pii': True, + 'traces_sample_rate': SENTRY_SAMPLE_RATE, + 'release': sentry.fetch_git_sha(BASE_DIR), + 'environment': GO_ENVIRONMENT, + 'debug': DEBUG, + 'tags': { + 'site': GO_API_FQDN, + }, +} +if SENTRY_DSN: + sentry.init_sentry( + app_type='API', + **SENTRY_CONFIG, + ) +# Required for Django HayStack +HAYSTACK_CONNECTIONS = { + 'default': { + 'ENGINE': 'haystack.backends.elasticsearch7_backend.Elasticsearch7SearchEngine', + 'URL': ELASTIC_SEARCH_HOST, + 'INDEX_NAME': 'new_index', + }, +} + +HAYSTACK_LIMIT_TO_REGISTERED_MODELS = False diff --git a/main/urls.py b/main/urls.py index af5f0cc76..df9bb8dc2 100644 --- a/main/urls.py +++ b/main/urls.py @@ -32,6 +32,7 @@ ShowUsername, EsPageSearch, EsPageHealth, + Brief, ERUTypes, RecentAffecteds, FieldReportStatuses, @@ -49,7 +50,8 @@ AddCronJobLog, DummyHttpStatusError, DummyExceptionError, - ResendValidation + ResendValidation, + HayStackSearch, ) from registrations.views import ( NewRegistration, @@ -69,6 +71,7 @@ ) from databank.views import CountryOverviewViewSet +from local_units.views import LocalUnitListAPIView, LocalUnitDetailAPIView # DRF routes from rest_framework import routers @@ -79,6 +82,7 @@ from deployments import drf_views as deployment_views from notifications import drf_views as notification_views from registrations import drf_views as registration_views +from country_plan import drf_views as country_plan_views from lang import views as lang_views from dref import views as dref_views @@ -116,7 +120,6 @@ router.register(r'percountry', per_views.FormCountryViewset, basename='percountry') router.register(r'perdata', per_views.FormDataViewset) router.register(r'perdocs', per_views.PERDocsViewset) -# router.register(r'percountryusers', per_views.FormCountryUsersViewset) router.register(r'peroverview', per_views.OverviewViewset, basename='peroverview') router.register(r'peroverviewstrict', per_views.OverviewStrictViewset, basename='peroverviewstrict') router.register(r'personnel_deployment', deployment_views.PersonnelDeploymentViewset, basename='personnel_deployment') @@ -162,12 +165,18 @@ router.register(r'dref-final-report', dref_views.DrefFinalReportViewSet, basename='dref_final_report') router.register(r'review-country', api_views.CountryOfFieldReportToReviewViewset, basename='review_country') +# Country Plan apis +router.register(r'country-plan', country_plan_views.CountryPlanViewset, basename='country_plan') + admin.site.site_header = 'IFRC Go administration' admin.site.site_title = 'IFRC Go admin' urlpatterns = [ url(r'^api/v1/es_search/', EsPageSearch.as_view()), + url(r'^api/v1/search/', HayStackSearch.as_view()), url(r'^api/v1/es_health/', EsPageHealth.as_view()), + # If we want to use the next one, some fixes needed, e.g. + # stackoverflow.com/questions/47166385/dont-know-how-to-convert-the-django-field-skills-class-taggit-managers-tagga url(r'^api/v1/graphql/', GraphQLView.as_view(graphiql=True)), url(r'^api/v1/aggregate/', AggregateByTime.as_view()), url(r'^api/v1/aggregate_dtype/', AggregateByDtype.as_view()), @@ -176,6 +185,7 @@ url(r'^api/v2/deployment/aggregated$', deployment_views.AggregateDeployments.as_view()), url(r'^api/v2/deployment/aggregated_by_month', deployment_views.DeploymentsByMonth.as_view()), url(r'^api/v2/deployment/aggregated_by_ns', deployment_views.DeploymentsByNS.as_view()), + url(r'^api/v2/brief', Brief.as_view()), url(r'^api/v2/erutype', ERUTypes.as_view()), url(r'^api/v2/recentaffected', RecentAffecteds.as_view()), url(r'^api/v2/fieldreportstatus', FieldReportStatuses.as_view()), @@ -213,6 +223,8 @@ url(r'^api/v2/event/(?P\d+)', api_views.EventViewset.as_view({'get': 'retrieve'})), url(r'^api/v2/event/(?P[-\w]+)', api_views.EventViewset.as_view({'get': 'retrieve'}, lookup_field='slug')), url(r'^api/v2/exportperresults/', per_views.ExportAssessmentToCSVViewset.as_view()), + url(r'^api/v2/local-unit/(?P\d+)', LocalUnitDetailAPIView.as_view()), + url(r'^api/v2/local-unit/', LocalUnitListAPIView.as_view()), url(r'^docs/', include_docs_urls(title='IFRC GO API', public=False)), url(r'^tinymce/', include('tinymce.urls')), url(r'^admin/', RedirectView.as_view(url='/')), diff --git a/main/utils.py b/main/utils.py index c6e3951cc..fd479f846 100644 --- a/main/utils.py +++ b/main/utils.py @@ -1,3 +1,6 @@ +import requests + +from tempfile import NamedTemporaryFile, _TemporaryFileWrapper from collections import defaultdict @@ -24,3 +27,36 @@ def get_merged_items_by_fields(items, fields, seperator=', '): field: seperator.join(data[field]) for field in fields } + + +class DownloadFileManager(): + """ + Convert Appeal API datetime into django datetime + Parameters + ---------- + url : str + Return: TemporaryFile + On close: Close and Delete the file + """ + def __init__(self, url, dir='/tmp/', **kwargs): + self.url = url + self.downloaded_file = None + # NamedTemporaryFile attributes + self.named_temporary_file_args = { + 'dir': dir, + **kwargs, + } + + def __enter__(self) -> _TemporaryFileWrapper: + file = NamedTemporaryFile(delete=True, **self.named_temporary_file_args) + with requests.get(self.url, stream=True) as r: + r.raise_for_status() + for chunk in r.iter_content(chunk_size=8192): + file.write(chunk) + file.flush() + self.downloaded_file = file + return self.downloaded_file + + def __exit__(self, *_): + if self.downloaded_file: + self.downloaded_file.close() diff --git a/mapbox/admin2/COL-centroids.json b/mapbox/admin2/COL-centroids.json index d70ab2b7a..1cfc86bb7 100644 --- a/mapbox/admin2/COL-centroids.json +++ b/mapbox/admin2/COL-centroids.json @@ -4,7 +4,7 @@ "go-admin2-COL-centroids": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-COL-centroids-src", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/COL-staging.json b/mapbox/admin2/COL-staging.json index 8ad42888f..380548371 100644 --- a/mapbox/admin2/COL-staging.json +++ b/mapbox/admin2/COL-staging.json @@ -4,7 +4,7 @@ "go-admin2-COL-staging": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-COL-src-staging", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/COL.json b/mapbox/admin2/COL.json index 8890b2df8..43e985cee 100644 --- a/mapbox/admin2/COL.json +++ b/mapbox/admin2/COL.json @@ -4,7 +4,7 @@ "go-admin2-COL": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-COL-src", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/GTM-centroids.json b/mapbox/admin2/GTM-centroids.json index ccc12743d..e1898b0df 100644 --- a/mapbox/admin2/GTM-centroids.json +++ b/mapbox/admin2/GTM-centroids.json @@ -4,7 +4,7 @@ "go-admin2-GTM-centroids": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-GTM-centroids-src", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/GTM-staging.json b/mapbox/admin2/GTM-staging.json index ad09c6fb3..58311d8d0 100644 --- a/mapbox/admin2/GTM-staging.json +++ b/mapbox/admin2/GTM-staging.json @@ -4,7 +4,7 @@ "go-admin2-GTM-staging": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-GTM-src-staging", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/GTM.json b/mapbox/admin2/GTM.json index 9dd0210f8..0376aa5c5 100644 --- a/mapbox/admin2/GTM.json +++ b/mapbox/admin2/GTM.json @@ -4,7 +4,7 @@ "go-admin2-GTM": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-GTM-src", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/HND-centroids.json b/mapbox/admin2/HND-centroids.json index a90e2d761..16695deee 100644 --- a/mapbox/admin2/HND-centroids.json +++ b/mapbox/admin2/HND-centroids.json @@ -4,7 +4,7 @@ "go-admin2-HND-centroids": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-HND-centroids-src", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/HND-staging.json b/mapbox/admin2/HND-staging.json index 81822fe6d..79c9f97da 100644 --- a/mapbox/admin2/HND-staging.json +++ b/mapbox/admin2/HND-staging.json @@ -4,7 +4,7 @@ "go-admin2-HND-staging": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-HND-src-staging", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/HND.json b/mapbox/admin2/HND.json index 611e49f6f..7a8a2d40a 100644 --- a/mapbox/admin2/HND.json +++ b/mapbox/admin2/HND.json @@ -4,7 +4,7 @@ "go-admin2-HND": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-HND-src", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/HTI-centroids.json b/mapbox/admin2/HTI-centroids.json index c001f0777..99800963c 100644 --- a/mapbox/admin2/HTI-centroids.json +++ b/mapbox/admin2/HTI-centroids.json @@ -4,7 +4,7 @@ "go-admin2-HTI-centroids": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-HTI-centroids-src", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/HTI-staging.json b/mapbox/admin2/HTI-staging.json index 2c0979457..63f8e3fe3 100644 --- a/mapbox/admin2/HTI-staging.json +++ b/mapbox/admin2/HTI-staging.json @@ -4,7 +4,7 @@ "go-admin2-HTI-staging": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-HTI-src-staging", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/HTI.json b/mapbox/admin2/HTI.json index 171b8786b..3317a511b 100644 --- a/mapbox/admin2/HTI.json +++ b/mapbox/admin2/HTI.json @@ -4,7 +4,7 @@ "go-admin2-HTI": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-HTI-src", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/IDN-centroids.json b/mapbox/admin2/IDN-centroids.json new file mode 100644 index 000000000..0baf68192 --- /dev/null +++ b/mapbox/admin2/IDN-centroids.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "layers": { + "go-admin2-IDN-centroids": { + "source": "mapbox://tileset-source/go-ifrc/go-admin2-IDN-centroids-src", + "minzoom": 5, + "maxzoom": 10 + } + } + } + \ No newline at end of file diff --git a/mapbox/admin2/IDN-staging.json b/mapbox/admin2/IDN-staging.json new file mode 100644 index 000000000..685c01e80 --- /dev/null +++ b/mapbox/admin2/IDN-staging.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "layers": { + "go-admin2-IDN-staging": { + "source": "mapbox://tileset-source/go-ifrc/go-admin2-IDN-src-staging", + "minzoom": 5, + "maxzoom": 10 + } + } + } + \ No newline at end of file diff --git a/mapbox/admin2/IDN.json b/mapbox/admin2/IDN.json new file mode 100644 index 000000000..3f1fa7ed1 --- /dev/null +++ b/mapbox/admin2/IDN.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "layers": { + "go-admin2-IDN": { + "source": "mapbox://tileset-source/go-ifrc/go-admin2-IDN-src", + "minzoom": 5, + "maxzoom": 10 + } + } + } + \ No newline at end of file diff --git a/mapbox/admin2/MYS-centroids.json b/mapbox/admin2/MYS-centroids.json new file mode 100644 index 000000000..910f92e8f --- /dev/null +++ b/mapbox/admin2/MYS-centroids.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "layers": { + "go-admin2-MYS-centroids": { + "source": "mapbox://tileset-source/go-ifrc/go-admin2-MYS-centroids-src", + "minzoom": 5, + "maxzoom": 10 + } + } + } + \ No newline at end of file diff --git a/mapbox/admin2/MYS-staging.json b/mapbox/admin2/MYS-staging.json new file mode 100644 index 000000000..ec5316b4e --- /dev/null +++ b/mapbox/admin2/MYS-staging.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "layers": { + "go-admin2-MYS-staging": { + "source": "mapbox://tileset-source/go-ifrc/go-admin2-MYS-src-staging", + "minzoom": 5, + "maxzoom": 10 + } + } + } + \ No newline at end of file diff --git a/mapbox/admin2/MYS.json b/mapbox/admin2/MYS.json new file mode 100644 index 000000000..f8c5d78e7 --- /dev/null +++ b/mapbox/admin2/MYS.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "layers": { + "go-admin2-MYS": { + "source": "mapbox://tileset-source/go-ifrc/go-admin2-MYS-src", + "minzoom": 5, + "maxzoom": 10 + } + } + } + \ No newline at end of file diff --git a/mapbox/admin2/PHL-centroids.json b/mapbox/admin2/PHL-centroids.json new file mode 100644 index 000000000..dd1c9f3e2 --- /dev/null +++ b/mapbox/admin2/PHL-centroids.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "layers": { + "go-admin2-PHL-centroids": { + "source": "mapbox://tileset-source/go-ifrc/go-admin2-PHL-centroids-src", + "minzoom": 5, + "maxzoom": 10 + } + } + } + \ No newline at end of file diff --git a/mapbox/admin2/PHL-staging.json b/mapbox/admin2/PHL-staging.json new file mode 100644 index 000000000..e61a4358f --- /dev/null +++ b/mapbox/admin2/PHL-staging.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "layers": { + "go-admin2-PHL-staging": { + "source": "mapbox://tileset-source/go-ifrc/go-admin2-PHL-src-staging", + "minzoom": 5, + "maxzoom": 10 + } + } + } + \ No newline at end of file diff --git a/mapbox/admin2/PHL.json b/mapbox/admin2/PHL.json new file mode 100644 index 000000000..7c2b47ef5 --- /dev/null +++ b/mapbox/admin2/PHL.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "layers": { + "go-admin2-PHL": { + "source": "mapbox://tileset-source/go-ifrc/go-admin2-PHL-src", + "minzoom": 5, + "maxzoom": 10 + } + } + } + \ No newline at end of file diff --git a/mapbox/admin2/POL-centroids.json b/mapbox/admin2/POL-centroids.json new file mode 100644 index 000000000..6cf4b4061 --- /dev/null +++ b/mapbox/admin2/POL-centroids.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "layers": { + "go-admin2-POL-centroids": { + "source": "mapbox://tileset-source/go-ifrc/go-admin2-POL-centroids-src", + "minzoom": 5, + "maxzoom": 10 + } + } + } + \ No newline at end of file diff --git a/mapbox/admin2/POL-staging.json b/mapbox/admin2/POL-staging.json new file mode 100644 index 000000000..1f463b0fc --- /dev/null +++ b/mapbox/admin2/POL-staging.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "layers": { + "go-admin2-POL-staging": { + "source": "mapbox://tileset-source/go-ifrc/go-admin2-POL-src-staging", + "minzoom": 5, + "maxzoom": 10 + } + } + } + \ No newline at end of file diff --git a/mapbox/admin2/POL.json b/mapbox/admin2/POL.json new file mode 100644 index 000000000..0daadc9e7 --- /dev/null +++ b/mapbox/admin2/POL.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "layers": { + "go-admin2-POL": { + "source": "mapbox://tileset-source/go-ifrc/go-admin2-POL-src", + "minzoom": 5, + "maxzoom": 10 + } + } + } + \ No newline at end of file diff --git a/mapbox/admin2/SLV-centroids.json b/mapbox/admin2/SLV-centroids.json index f281f75b3..1dd6820ec 100644 --- a/mapbox/admin2/SLV-centroids.json +++ b/mapbox/admin2/SLV-centroids.json @@ -4,7 +4,7 @@ "go-admin2-SLV-centroids": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-SLV-centroids-src", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/SLV-staging.json b/mapbox/admin2/SLV-staging.json index 89234e206..310cc5ea1 100644 --- a/mapbox/admin2/SLV-staging.json +++ b/mapbox/admin2/SLV-staging.json @@ -4,7 +4,7 @@ "go-admin2-SLV-staging": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-SLV-src-staging", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/SLV.json b/mapbox/admin2/SLV.json index 92271cd09..084e5ff2b 100644 --- a/mapbox/admin2/SLV.json +++ b/mapbox/admin2/SLV.json @@ -4,7 +4,7 @@ "go-admin2-SLV": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-SLV-src", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/SYR-centroids.json b/mapbox/admin2/SYR-centroids.json new file mode 100644 index 000000000..9b4b5953f --- /dev/null +++ b/mapbox/admin2/SYR-centroids.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "layers": { + "go-admin2-SYR-centroids": { + "source": "mapbox://tileset-source/go-ifrc/go-admin2-SYR-centroids-src", + "minzoom": 5, + "maxzoom": 10 + } + } + } + \ No newline at end of file diff --git a/mapbox/admin2/SYR-staging.json b/mapbox/admin2/SYR-staging.json new file mode 100644 index 000000000..e358d7244 --- /dev/null +++ b/mapbox/admin2/SYR-staging.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "layers": { + "go-admin2-SYR-staging": { + "source": "mapbox://tileset-source/go-ifrc/go-admin2-SYR-src-staging", + "minzoom": 5, + "maxzoom": 10 + } + } + } + \ No newline at end of file diff --git a/mapbox/admin2/SYR.json b/mapbox/admin2/SYR.json new file mode 100644 index 000000000..357f664ae --- /dev/null +++ b/mapbox/admin2/SYR.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "layers": { + "go-admin2-SYR": { + "source": "mapbox://tileset-source/go-ifrc/go-admin2-SYR-src", + "minzoom": 5, + "maxzoom": 10 + } + } + } + \ No newline at end of file diff --git a/mapbox/admin2/TUR-centroids.json b/mapbox/admin2/TUR-centroids.json new file mode 100644 index 000000000..d7fb79994 --- /dev/null +++ b/mapbox/admin2/TUR-centroids.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "layers": { + "go-admin2-TUR-centroids": { + "source": "mapbox://tileset-source/go-ifrc/go-admin2-TUR-centroids-src", + "minzoom": 5, + "maxzoom": 10 + } + } + } + \ No newline at end of file diff --git a/mapbox/admin2/TUR-staging.json b/mapbox/admin2/TUR-staging.json new file mode 100644 index 000000000..55ff8ada7 --- /dev/null +++ b/mapbox/admin2/TUR-staging.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "layers": { + "go-admin2-TUR-staging": { + "source": "mapbox://tileset-source/go-ifrc/go-admin2-TUR-src-staging", + "minzoom": 5, + "maxzoom": 10 + } + } + } + \ No newline at end of file diff --git a/mapbox/admin2/TUR.json b/mapbox/admin2/TUR.json new file mode 100644 index 000000000..3a8cffb9d --- /dev/null +++ b/mapbox/admin2/TUR.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "layers": { + "go-admin2-TUR": { + "source": "mapbox://tileset-source/go-ifrc/go-admin2-TUR-src", + "minzoom": 5, + "maxzoom": 10 + } + } + } + \ No newline at end of file diff --git a/mapbox/admin2/UKR-centroids.json b/mapbox/admin2/UKR-centroids.json index c2c36d793..cba907762 100644 --- a/mapbox/admin2/UKR-centroids.json +++ b/mapbox/admin2/UKR-centroids.json @@ -4,7 +4,7 @@ "go-admin2-UKR-centroids": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-UKR-centroids-src", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/UKR-staging.json b/mapbox/admin2/UKR-staging.json index b2565a164..6118b1384 100644 --- a/mapbox/admin2/UKR-staging.json +++ b/mapbox/admin2/UKR-staging.json @@ -4,7 +4,7 @@ "go-admin2-UKR-staging": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-UKR-src-staging", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/UKR.json b/mapbox/admin2/UKR.json index b87546a39..1773eeb01 100644 --- a/mapbox/admin2/UKR.json +++ b/mapbox/admin2/UKR.json @@ -4,7 +4,7 @@ "go-admin2-UKR": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-UKR-src", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/VEN-centroids.json b/mapbox/admin2/VEN-centroids.json index 67b0dbc4b..28eb077d9 100644 --- a/mapbox/admin2/VEN-centroids.json +++ b/mapbox/admin2/VEN-centroids.json @@ -4,7 +4,7 @@ "go-admin2-VEN-centroids": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-VEN-centroids-src", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/VEN-staging.json b/mapbox/admin2/VEN-staging.json index b9a05d5e1..36dfb1a1f 100644 --- a/mapbox/admin2/VEN-staging.json +++ b/mapbox/admin2/VEN-staging.json @@ -4,7 +4,7 @@ "go-admin2-VEN-staging": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-VEN-src-staging", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/mapbox/admin2/VEN.json b/mapbox/admin2/VEN.json index 2971f3a4e..77942c9dd 100644 --- a/mapbox/admin2/VEN.json +++ b/mapbox/admin2/VEN.json @@ -4,7 +4,7 @@ "go-admin2-VEN": { "source": "mapbox://tileset-source/go-ifrc/go-admin2-VEN-src", "minzoom": 5, - "maxzoom": 14 + "maxzoom": 10 } } } diff --git a/middlewares/middlewares.py b/middlewares/middlewares.py index d2847f16b..c74e43758 100644 --- a/middlewares/middlewares.py +++ b/middlewares/middlewares.py @@ -30,6 +30,7 @@ def get_username(): class RequestMiddleware: + def __init__(self, get_response): self.get_response = get_response # One-time configuration and initialization. @@ -51,7 +52,8 @@ def __call__(self, request): setattr(_threadlocal, "request", request) return self.get_response(request) - def process_view(self, request, view_function, *args, **kwargs): + @staticmethod + def process_view(request, view_function, *args, **kwargs): # NOTE: For POST raise error on non default language request_match = request.resolver_match reject_the_request = ( diff --git a/notifications/drf_views.py b/notifications/drf_views.py index 4c3f24b5b..1c9b78211 100644 --- a/notifications/drf_views.py +++ b/notifications/drf_views.py @@ -42,7 +42,9 @@ def get_queryset(self): limit = 14 # days cond1 = Q(is_stood_down=True) cond2 = Q(end__lt=datetime.utcnow().replace(tzinfo=timezone.utc)-timedelta(days=limit)) - return SurgeAlert.objects.exclude(cond1 & cond2) + return super().get_queryset().\ + select_related('country').\ + exclude(cond1 & cond2) # 'event' inclusion ^ to _related needs frontend change, otherwise the Position column shows garbage in /alerts/all class SubscriptionViewset(viewsets.ModelViewSet): diff --git a/notifications/models.py b/notifications/models.py index 3b70aa380..36a0d1cf6 100644 --- a/notifications/models.py +++ b/notifications/models.py @@ -38,7 +38,7 @@ class SurgeAlert(models.Model): molnix_id = models.IntegerField(blank=True, null=True) # Status field from Molnix - `unfilled` denotes Stood-Down - molnix_status = models.CharField(blank=True, null=True, max_length=32) + molnix_status = models.CharField(blank=True, null=True, max_length=32) # It depends on molnix_status. Check "save" method below. is_stood_down = models.BooleanField(verbose_name=_('is stood down?'), default=False) @@ -69,8 +69,12 @@ def save(self, *args, **kwargs): def __str__(self): if self.operation and self.operation != '': return self.operation - else: + elif self.event: return self.event.name + elif self.message: + return self.message + else: + return '–' class SubscriptionType(models.IntegerChoices): diff --git a/notifications/notification.py b/notifications/notification.py index 2470eaf6f..1394b46f8 100644 --- a/notifications/notification.py +++ b/notifications/notification.py @@ -85,26 +85,25 @@ def send_notification(subject, recipients, html, mailtype='', files=None): print(f'\n{html}\n') print('-' * 22, 'EMAIL END -', '-' * 22) - # If it's not PROD only able to use test e-mail addresses which are set in the env var to_addresses = recipients if isinstance(recipients, list) else [recipients] - if not IS_PROD: - logger.info('Using test email addresses...') - to_addresses = [] - logger.info(to_addresses) - for eml in settings.TEST_EMAILS: - - # It is possible to filter test addressees to domain name only – not used. - is_dom = True if '@' not in eml else False - if is_dom: - for rec in recipients: - try: - if eml == rec.split('@')[1]: - to_addresses.append(rec) - except Exception: - logger.info('Could not extract domain from: {}'.format(rec)) - elif eml and (eml in recipients): - to_addresses.append(eml) +# if not IS_PROD: +# logger.info('Using test email addresses...') +# to_addresses = [] +# logger.info(to_addresses) +# for eml in settings.TEST_EMAILS: +# +# # It is possible to filter test addressees to domain name only – not used. +# is_dom = True if '@' not in eml else False +# if is_dom: +# for rec in recipients: +# try: +# if eml == rec.split('@')[1]: +# to_addresses.append(rec) +# except Exception: +# logger.info('Could not extract domain from: {}'.format(rec)) +# elif eml and (eml in recipients): +# to_addresses.append(eml) recipients_as_string = ','.join(to_addresses) if not recipients_as_string: diff --git a/notifications/search_indexes.py b/notifications/search_indexes.py new file mode 100644 index 000000000..3fef6d489 --- /dev/null +++ b/notifications/search_indexes.py @@ -0,0 +1,27 @@ +from haystack import indexes + +from notifications.models import SurgeAlert + + +class SurgeIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.EdgeNgramField(document=True, use_template=True) + name = indexes.EdgeNgramField(model_attr='message') + molnix_tag = indexes.MultiValueField(null=True) + event_name = indexes.CharField(model_attr='event__name') + country_name = indexes.CharField(model_attr='country__name') + start_date = indexes.DateTimeField(model_attr='start', null=True) + alert_date = indexes.DateTimeField(model_attr='opens', null=True) + event_id = indexes.IntegerField(model_attr='event__id', null=True) + deadline = indexes.DateTimeField(model_attr='closes', null=True) + surge_type = indexes.CharField(model_attr='get_atype_display') + status = indexes.CharField(model_attr='molnix_status', null=True) + country_id = indexes.IntegerField(model_attr='country__id') + + def get_model(self): + return SurgeAlert + + def prepare_molnix_tag(self, obj): + return [molnix.name for molnix in obj.molnix_tags.all()] + + def index_queryset(self, using=None): + return self.get_model().objects.all() diff --git a/notifications/serializers.py b/notifications/serializers.py index b7bda6ed2..56388fa11 100644 --- a/notifications/serializers.py +++ b/notifications/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from api.serializers import MiniEventSerializer, ListEventSerializer, MiniCountrySerializer +from api.serializers import MiniEventSerializer, SurgeEventSerializer, MiniCountrySerializer from lang.serializers import ModelSerializer from .models import SurgeAlert, Subscription @@ -8,7 +8,7 @@ class SurgeAlertSerializer(ModelSerializer): - event = ListEventSerializer() + event = SurgeEventSerializer() country = MiniCountrySerializer() atype_display = serializers.CharField(source='get_atype_display', read_only=True) category_display = serializers.CharField(source='get_category_display', read_only=True) diff --git a/notifications/templates/design/main_field_report.html b/notifications/templates/design/main_field_report.html index 847098c0b..79f6af76c 100644 --- a/notifications/templates/design/main_field_report.html +++ b/notifications/templates/design/main_field_report.html @@ -260,7 +260,7 @@ National Society: {{ fr.ns_assistance }} - {% if fr.other_fields.bulletin %} + {% if fr.other_fields.bulletin != 'No' %} Information Bulletin: {{ fr.other_fields.bulletin }} @@ -343,7 +343,7 @@ - {% if fr.other_fields.dref %} + {% if fr.other_fields.dref != 'No' %} {% endif %} - {% if fr.other_fields.imminent_dref %} + {% if fr.other_fields.imminent_dref != 'No' %} {% endif %} - {% if fr.other_fields.eru_base_camp %} + {% if fr.other_fields.eru_base_camp != 'No' %} {% endif %} - {% if fr.other_fields.eru_basic_health_care %} + {% if fr.other_fields.eru_basic_health_care != 'No' %} {% endif %} - {% if fr.other_fields.eru_deployment_hospital %} + {% if fr.other_fields.eru_deployment_hospital != 'No' %} {% endif %} - {% if fr.other_fields.eru_it_telecom %} + {% if fr.other_fields.eru_it_telecom != 'No' %} {% endif %} - {% if fr.other_fields.eru_logistics %} + {% if fr.other_fields.eru_logistics != 'No' %} {% endif %} - {% if fr.other_fields.eru_referral_hospital %} + {% if fr.other_fields.eru_referral_hospital != 'No' %} {% endif %} - {% if fr.other_fields.eru_relief %} + {% if fr.other_fields.eru_relief != 'No' %} {% endif %} - {% if fr.other_fields.eru_water_sanitation_15 %} + {% if fr.other_fields.eru_water_sanitation_15 != 'No' %} {% endif %} - {% if fr.other_fields.eru_water_sanitation_20 %} + {% if fr.other_fields.eru_water_sanitation_20 != 'No' %} {% endif %} - {% if fr.other_fields.eru_water_sanitation_40 %} + {% if fr.other_fields.eru_water_sanitation_40 != 'No' %} {% endif %} - {% if fr.other_fields.forecast_based_action %} + {% if fr.other_fields.forecast_based_action != 'No' %} {% endif %} 581 - 733 {% endcomment %} - {% if fr.other_fields.appeal.name != 'NO' %} + {% if fr.other_fields.appeal != 'No' %} + {{ fr.other_fields.appeal }} {% endif %} {% if fr.other_fields.appeal_amount %} diff --git a/notifications/templates/search/indexes/notifications/surgealert_text.txt b/notifications/templates/search/indexes/notifications/surgealert_text.txt new file mode 100644 index 000000000..f353c4f6f --- /dev/null +++ b/notifications/templates/search/indexes/notifications/surgealert_text.txt @@ -0,0 +1,10 @@ +{{object.name}} +{{object.event_name}} +{{object.country_name}} +{{object.start_date}} +{{object.alert_date}} +{{object.deadline}} +{{object.status}} +{% for molnix in object.molnix_tag_set.all %} + {{ molnix.name}} +{% endfor %} \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 0b7753a4b..8da6db1c2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -27,32 +27,29 @@ python-versions = "*" [[package]] name = "arabic-reshaper" -version = "2.1.4" -description = "Reconstruct Arabic sentences to be used in applications that don't support Arabic" +version = "3.0.0" +description = "Reconstruct Arabic sentences to be used in applications that do not support Arabic" category = "main" optional = false python-versions = "*" -[package.dependencies] -future = "*" - [package.extras] -with-fonttools = ["fonttools (>=3.0,<4.0)", "fonttools (>=4.0)"] +with-fonttools = ["fonttools (>=4.0)"] [[package]] name = "asgiref" -version = "3.5.2" +version = "3.6.0" description = "ASGI specs, helper code, and adapters" category = "main" optional = false python-versions = ">=3.7" [package.extras] -tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] [[package]] name = "asttokens" -version = "2.0.8" +version = "2.2.1" description = "Annotate AST trees with source code positions" category = "main" optional = false @@ -62,7 +59,7 @@ python-versions = "*" six = "*" [package.extras] -test = ["astroid (<=2.5.3)", "pytest"] +test = ["astroid", "pytest"] [[package]] name = "async-timeout" @@ -74,17 +71,18 @@ python-versions = ">=3.6" [[package]] name = "attrs" -version = "22.1.0" +version = "22.2.0" description = "Classes Without Boilerplate" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] +cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] +tests = ["attrs[tests-no-zope]", "zope.interface"] +tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] [[package]] name = "azure-common" @@ -227,7 +225,7 @@ crt = ["awscrt (==0.12.5)"] [[package]] name = "cachetools" -version = "5.2.0" +version = "5.3.0" description = "Extensible memoizing collections and decorators" category = "main" optional = false @@ -250,6 +248,7 @@ click-repl = ">=0.1.6" kombu = ">=5.1.0,<6.0" pytz = ">0.0-dev" redis = {version = ">=3.2.0", optional = true, markers = "extra == \"redis\""} +setuptools = "*" vine = ">=5.0.0,<6.0" [package.extras] @@ -287,7 +286,7 @@ zstd = ["zstandard"] [[package]] name = "certifi" -version = "2022.9.24" +version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -306,11 +305,11 @@ pycparser = "*" [[package]] name = "chardet" -version = "5.0.0" +version = "5.1.0" description = "Universal encoding detector for Python 3" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "charset-normalizer" @@ -321,7 +320,7 @@ optional = false python-versions = ">=3.6.0" [package.extras] -unicode_backport = ["unicodedata2"] +unicode-backport = ["unicodedata2"] [[package]] name = "choicesenum" @@ -365,7 +364,7 @@ python-versions = "*" click = ">=4.0" [package.extras] -dev = ["pytest (>=3.6)", "pytest-cov", "wheel", "coveralls"] +dev = ["coveralls", "pytest (>=3.6)", "pytest-cov", "wheel"] [[package]] name = "click-repl" @@ -396,11 +395,11 @@ test = ["pytest-cov"] [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "coreapi" @@ -437,7 +436,7 @@ python-versions = "*" [[package]] name = "cryptography" -version = "38.0.1" +version = "39.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -447,12 +446,14 @@ python-versions = ">=3.6" cffi = ">=1.12" [package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] -pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +pep8test = ["black", "check-manifest", "mypy", "ruff", "types-pytz", "types-requests"] sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist", "pytz"] +test-randomorder = ["pytest-randomly"] +tox = ["tox"] [[package]] name = "decorator" @@ -462,20 +463,6 @@ category = "main" optional = false python-versions = ">=3.5" -[[package]] -name = "deprecated" -version = "1.2.13" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -wrapt = ">=1.10,<2" - -[package.extras] -dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"] - [[package]] name = "distro" version = "1.8.0" @@ -486,7 +473,7 @@ python-versions = ">=3.6" [[package]] name = "django" -version = "3.2.16" +version = "3.2.18" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false @@ -541,15 +528,15 @@ python-versions = "*" [[package]] name = "django-debug-toolbar" -version = "2.2.1" +version = "3.8.1" description = "A configurable set of panels that display various debug information about the current request/response." category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" [package.dependencies] -Django = ">=1.11" -sqlparse = ">=0.2.0" +django = ">=3.2.4" +sqlparse = ">=0.2" [[package]] name = "django-enumfield" @@ -563,7 +550,7 @@ python-versions = "*" six = ">=1.10.0" [package.extras] -dev = ["black", "isort", "django", "mypy", "django-stubs", "djangorestframework-stubs"] +dev = ["Django", "black", "django-stubs", "djangorestframework-stubs", "isort", "mypy"] [[package]] name = "django-environ" @@ -574,7 +561,7 @@ optional = false python-versions = ">=3.4,<4" [package.extras] -develop = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)", "furo (>=2021.8.17b43,<2021.9.0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] +develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.0)", "pytest (>=4.6.11)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] docs = ["furo (>=2021.8.17b43,<2021.9.0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] @@ -611,6 +598,21 @@ python-versions = ">=3.5" [package.dependencies] Django = ">=2.2" +[[package]] +name = "django-haystack" +version = "3.2.1" +description = "Pluggable search for Django." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +Django = ">=2.2" +elasticsearch = {version = ">=5,<8", optional = true, markers = "extra == \"elasticsearch\""} + +[package.extras] +elasticsearch = ["elasticsearch (>=5,<8)"] + [[package]] name = "django-modeltranslation" version = "0.17.5" @@ -736,29 +738,48 @@ djangorestframework = "*" [[package]] name = "elasticsearch" -version = "6.8.2" +version = "7.0.0" description = "Python client for Elasticsearch" category = "main" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" +python-versions = "*" [package.dependencies] urllib3 = ">=1.21.1" [package.extras] -develop = ["requests (>=2.0.0,<3.0.0)", "nose", "coverage", "mock", "pyyaml", "nosexcover", "numpy", "pandas", "sphinx (<1.7)", "sphinx-rtd-theme"] +develop = ["coverage", "mock", "nose", "nosexcover", "pyaml", "requests (>=2.0.0,<3.0.0)", "sphinx (<1.7)", "sphinx-rtd-theme"] requests = ["requests (>=2.4.0,<3.0.0)"] +[[package]] +name = "et-xmlfile" +version = "1.1.0" +description = "An implementation of lxml.xmlfile for the standard library" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "exceptiongroup" +version = "1.1.0" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "executing" -version = "1.1.1" +version = "1.2.0" description = "Get the currently executing AST node of a frame, and other information" category = "main" optional = false python-versions = "*" [package.extras] -tests = ["rich", "littleutils", "pytest", "asttokens"] +tests = ["asttokens", "littleutils", "pytest", "rich"] [[package]] name = "factory-boy" @@ -773,7 +794,7 @@ Faker = ">=0.7.0" [[package]] name = "faker" -version = "15.1.1" +version = "17.0.0" description = "Faker is a Python package that generates fake data for you." category = "main" optional = false @@ -794,14 +815,6 @@ python-versions = "*" wasmer = ">=1.0.0" wasmer-compiler-cranelift = ">=1.0.0" -[[package]] -name = "future" -version = "0.18.2" -description = "Clean single-source support for Python 3 and 2" -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - [[package]] name = "fuzzywuzzy" version = "0.17.0" @@ -815,26 +828,26 @@ speedup = ["python-levenshtein (>=0.12)"] [[package]] name = "google-api-core" -version = "2.10.2" +version = "2.11.0" description = "Google API client core library" category = "main" optional = false python-versions = ">=3.7" [package.dependencies] -google-auth = ">=1.25.0,<3.0dev" +google-auth = ">=2.14.1,<3.0dev" googleapis-common-protos = ">=1.56.2,<2.0dev" protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0dev" requests = ">=2.18.0,<3.0.0dev" [package.extras] -grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio-status (>=1.33.2,<2.0dev)"] +grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0dev)", "grpcio-status (>=1.49.1,<2.0dev)"] grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0dev)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0dev)"] [[package]] name = "google-auth" -version = "2.13.0" +version = "2.16.1" description = "Google Authentication Library" category = "main" optional = false @@ -847,24 +860,25 @@ rsa = {version = ">=3.1.4,<5", markers = "python_version >= \"3.6\""} six = ">=1.9.0" [package.extras] -aiohttp = ["requests (>=2.20.0,<3.0.0dev)", "aiohttp (>=3.6.2,<4.0.0dev)"] -enterprise_cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"] -pyopenssl = ["pyopenssl (>=20.0.0)"] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)", "requests (>=2.20.0,<3.0.0dev)"] +enterprise-cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"] +pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] reauth = ["pyu2f (>=0.1.5)"] +requests = ["requests (>=2.20.0,<3.0.0dev)"] [[package]] name = "googleapis-common-protos" -version = "1.56.4" +version = "1.58.0" description = "Common protobufs used in Google APIs" category = "main" optional = false python-versions = ">=3.7" [package.dependencies] -protobuf = ">=3.15.0,<5.0.0dev" +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0dev" [package.extras] -grpc = ["grpcio (>=1.0.0,<2.0.0dev)"] +grpc = ["grpcio (>=1.44.0,<2.0.0dev)"] [[package]] name = "gprof2dot" @@ -891,7 +905,7 @@ six = ">=1.10.0,<2" [package.extras] django = ["graphene-django"] sqlalchemy = ["graphene-sqlalchemy"] -test = ["pytest", "pytest-benchmark", "pytest-cov", "pytest-mock", "fastdiff (==0.2.0)", "snapshottest", "coveralls", "promise", "six", "mock", "pytz", "iso8601"] +test = ["coveralls", "fastdiff (==0.2.0)", "iso8601", "mock", "promise", "pytest", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytz", "six", "snapshottest"] [[package]] name = "graphene-django" @@ -911,9 +925,9 @@ six = ">=1.10.0" text-unidecode = "*" [package.extras] -dev = ["black (==19.10b0)", "flake8 (==3.7.9)", "flake8-black (==0.1.1)", "flake8-bugbear (==20.1.4)", "pytest (>=3.6.3)", "pytest-cov", "coveralls", "mock", "pytz", "pytest-django (>=3.3.2)", "djangorestframework (>=3.6.3)", "django-filter (<2)", "django-filter (>=2)"] -rest_framework = ["djangorestframework (>=3.6.3)"] -test = ["pytest (>=3.6.3)", "pytest-cov", "coveralls", "mock", "pytz", "pytest-django (>=3.3.2)", "djangorestframework (>=3.6.3)", "django-filter (<2)", "django-filter (>=2)"] +dev = ["black (==19.10b0)", "coveralls", "django-filter (<2)", "django-filter (>=2)", "djangorestframework (>=3.6.3)", "flake8 (==3.7.9)", "flake8-black (==0.1.1)", "flake8-bugbear (==20.1.4)", "mock", "pytest (>=3.6.3)", "pytest-cov", "pytest-django (>=3.3.2)", "pytz"] +rest-framework = ["djangorestframework (>=3.6.3)"] +test = ["coveralls", "django-filter (<2)", "django-filter (>=2)", "djangorestframework (>=3.6.3)", "mock", "pytest (>=3.6.3)", "pytest-cov", "pytest-django (>=3.3.2)", "pytz"] [[package]] name = "graphql-core" @@ -930,7 +944,7 @@ six = ">=1.10.0" [package.extras] gevent = ["gevent (>=1.1)"] -test = ["six (==1.14.0)", "pyannotate (==1.2.0)", "pytest (==4.6.10)", "pytest-django (==3.9.0)", "pytest-cov (==2.8.1)", "coveralls (==1.11.1)", "cython (==0.29.17)", "gevent (==1.5.0)", "pytest-benchmark (==3.2.3)", "pytest-mock (==2.0.0)"] +test = ["coveralls (==1.11.1)", "cython (==0.29.17)", "gevent (==1.5.0)", "pyannotate (==1.2.0)", "pytest (==4.6.10)", "pytest-benchmark (==3.2.3)", "pytest-cov (==2.8.1)", "pytest-django (==3.9.0)", "pytest-mock (==2.0.0)", "six (==1.14.0)"] [[package]] name = "graphql-relay" @@ -966,7 +980,7 @@ six = ">=1.9" webencodings = "*" [package.extras] -all = ["genshi", "chardet (>=2.2)", "lxml"] +all = ["chardet (>=2.2)", "genshi", "lxml"] chardet = ["chardet (>=2.2)"] genshi = ["genshi"] lxml = ["lxml"] @@ -981,15 +995,15 @@ python-versions = ">=3.5" [[package]] name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" [[package]] name = "ipython" -version = "8.5.0" +version = "8.10.0" description = "IPython: Productive Interactive Computing" category = "main" optional = false @@ -1004,15 +1018,15 @@ jedi = ">=0.16" matplotlib-inline = "*" pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} pickleshare = "*" -prompt-toolkit = ">3.0.1,<3.1.0" +prompt-toolkit = ">=3.0.30,<3.1.0" pygments = ">=2.4.0" stack-data = "*" traitlets = ">=5" [package.extras] -all = ["black", "Sphinx (>=1.3)", "ipykernel", "nbconvert", "nbformat", "ipywidgets", "notebook", "ipyparallel", "qtconsole", "pytest (<7.1)", "pytest-asyncio", "testpath", "curio", "matplotlib (!=3.2.0)", "numpy (>=1.19)", "pandas", "trio"] +all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] black = ["black"] -doc = ["Sphinx (>=1.3)"] +doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] kernel = ["ipykernel"] nbconvert = ["nbconvert"] nbformat = ["nbformat"] @@ -1020,7 +1034,7 @@ notebook = ["ipywidgets", "notebook"] parallel = ["ipyparallel"] qtconsole = ["qtconsole"] test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] -test_extra = ["pytest (<7.1)", "pytest-asyncio", "testpath", "curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.19)", "pandas", "trio"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] [[package]] name = "itypes" @@ -1032,7 +1046,7 @@ python-versions = "*" [[package]] name = "jedi" -version = "0.18.1" +version = "0.18.2" description = "An autocompletion tool for Python that can be used for text editors." category = "main" optional = false @@ -1042,8 +1056,9 @@ python-versions = ">=3.6" parso = ">=0.8.0,<0.9.0" [package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] -testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<7.0.0)"] +testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] [[package]] name = "jinja2" @@ -1086,11 +1101,12 @@ python-versions = "*" [package.dependencies] attrs = ">=17.4.0" pyrsistent = ">=0.14.0" +setuptools = "*" six = ">=1.11.0" [package.extras] format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] -format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"] +format-nongpl = ["idna", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "webcolors"] [[package]] name = "jsonseq" @@ -1143,7 +1159,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" [package.extras] cssselect = ["cssselect (>=0.7)"] html5 = ["html5lib"] -htmlsoup = ["beautifulsoup4"] +htmlsoup = ["BeautifulSoup4"] source = ["Cython (>=0.29.7)"] [[package]] @@ -1167,7 +1183,7 @@ requests-toolbelt = "*" [package.extras] estimate-area = ["supermercado (>=0.2.0,<0.3.0)"] -test = ["codecov", "pytest (==4.6.11)", "pytest-cov", "pre-commit", "black (==20.8b1)", "pep8", "supermercado (>=0.2.0,<0.3.0)", "toml (==0.10.2)"] +test = ["black (==20.8b1)", "codecov", "pep8", "pre-commit", "pytest (==4.6.11)", "pytest-cov", "supermercado (>=0.2.0,<0.3.0)", "toml (==0.10.2)"] [[package]] name = "markdown" @@ -1182,7 +1198,7 @@ testing = ["coverage", "pyyaml"] [[package]] name = "markupsafe" -version = "2.1.1" +version = "2.1.2" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false @@ -1212,19 +1228,19 @@ click = ">=3.0" [package.extras] dev = ["check-manifest"] -test = ["coveralls", "hypothesis", "pytest-cov", "pydocstyle"] +test = ["coveralls", "hypothesis", "pydocstyle", "pytest-cov"] [[package]] name = "numpy" -version = "1.23.4" -description = "NumPy is the fundamental package for array computing with Python." +version = "1.24.2" +description = "Fundamental package for array computing in Python" category = "main" optional = false python-versions = ">=3.8" [[package]] name = "opencensus" -version = "0.11.0" +version = "0.11.1" description = "A stats collection and distributed tracing framework" category = "main" optional = false @@ -1268,15 +1284,23 @@ Django = ">=1.11" opencensus = ">=0.7.12,<1.0.0" [[package]] -name = "packaging" -version = "21.3" -description = "Core utilities for Python packages" +name = "openpyxl" +version = "3.0.10" +description = "A Python library to read/write Excel 2010 xlsx/xlsm files" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +et-xmlfile = "*" + +[[package]] +name = "packaging" +version = "23.0" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" [[package]] name = "pandas" @@ -1323,7 +1347,7 @@ python-versions = "*" pillow = "*" [[package]] -name = "pdfminer.six" +name = "pdfminer-six" version = "20191110" description = "PDF parser and analyzer" category = "main" @@ -1361,12 +1385,16 @@ python-versions = "*" [[package]] name = "pillow" -version = "9.0.1" +version = "9.3.0" description = "Python Imaging Library (Fork)" category = "main" optional = false python-versions = ">=3.7" +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + [[package]] name = "pluggy" version = "1.0.0" @@ -1399,22 +1427,22 @@ python-versions = "*" six = "*" [package.extras] -test = ["pytest (>=2.7.3)", "pytest-cov", "coveralls", "futures", "pytest-benchmark", "mock"] +test = ["coveralls", "futures", "mock", "pytest (>=2.7.3)", "pytest-benchmark", "pytest-cov"] [[package]] name = "prompt-toolkit" -version = "3.0.31" +version = "3.0.37" description = "Library for building powerful interactive command lines in Python" category = "main" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.7.0" [package.dependencies] wcwidth = "*" [[package]] name = "protobuf" -version = "4.21.8" +version = "4.22.0" description = "" category = "main" optional = false @@ -1422,14 +1450,14 @@ python-versions = ">=3.7" [[package]] name = "psutil" -version = "5.9.3" +version = "5.9.4" description = "Cross-platform lib for process and system monitoring in Python." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.extras] -test = ["ipaddress", "mock", "enum34", "pywin32", "wmi"] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] [[package]] name = "psycopg2" @@ -1458,14 +1486,6 @@ python-versions = "*" [package.extras] tests = ["pytest"] -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - [[package]] name = "pyasn1" version = "0.4.8" @@ -1503,7 +1523,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pycryptodome" -version = "3.15.0" +version = "3.17" description = "Cryptographic library for Python" category = "main" optional = false @@ -1518,11 +1538,11 @@ optional = false python-versions = "*" [package.extras] -dev = ["coverage", "flake8", "mock", "pylint", "pytest", "pytest-cov", "sphinx", "sphinx-rtd-theme", "tox", "twine", "wheel"] +dev = ["Sphinx", "coverage", "flake8", "mock", "pylint", "pytest", "pytest-cov", "sphinx-rtd-theme", "tox", "twine", "wheel"] [[package]] name = "pygments" -version = "2.13.0" +version = "2.14.0" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false @@ -1531,17 +1551,6 @@ python-versions = ">=3.6" [package.extras] plugins = ["importlib-metadata"] -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" -optional = false -python-versions = ">=3.6.8" - -[package.extras] -diagrams = ["railroad-diagrams", "jinja2"] - [[package]] name = "pypdf2" version = "1.26.0" @@ -1552,7 +1561,7 @@ python-versions = "*" [[package]] name = "pyrsistent" -version = "0.18.1" +version = "0.19.3" description = "Persistent/Functional/Immutable data structures" category = "main" optional = false @@ -1560,7 +1569,7 @@ python-versions = ">=3.7" [[package]] name = "pytest" -version = "7.1.3" +version = "7.2.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -1569,11 +1578,11 @@ python-versions = ">=3.7" [package.dependencies] attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] @@ -1591,7 +1600,7 @@ pytest = ">=5.4.0" [package.extras] docs = ["sphinx", "sphinx-rtd-theme"] -testing = ["django", "django-configurations (>=2.0)"] +testing = ["Django", "django-configurations (>=2.0)"] [[package]] name = "pytest-ordering" @@ -1650,6 +1659,9 @@ category = "main" optional = false python-versions = "*" +[package.dependencies] +setuptools = "*" + [[package]] name = "python-mimeparse" version = "1.6.0" @@ -1676,16 +1688,14 @@ python-versions = "*" [[package]] name = "redis" -version = "4.3.4" +version = "4.5.1" description = "Python client for Redis database and key-value store" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] async-timeout = ">=4.0.2" -deprecated = ">=1.2.3" -packaging = ">=20.4" [package.extras] hiredis = ["hiredis (>=1.0.0)"] @@ -1721,11 +1731,11 @@ urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-toolbelt" -version = "0.10.0" +version = "0.10.1" description = "A utility belt for advanced users of python-requests" category = "main" optional = false @@ -1747,7 +1757,7 @@ pyasn1 = ">=0.1.3" [[package]] name = "rx" -version = "1.6.1" +version = "1.6.3" description = "Reactive Extensions (Rx) for Python" category = "main" optional = false @@ -1768,19 +1778,65 @@ botocore = ">=1.12.36,<2.0a.0" crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] [[package]] -name = "singledispatch" -version = "3.7.0" -description = "Backport functools.singledispatch from Python 3.4 to Python 2.6-3.3." +name = "sentry-sdk" +version = "1.15.0" +description = "Python client for Sentry (https://sentry.io)" category = "main" optional = false -python-versions = ">=2.6" +python-versions = "*" [package.dependencies] -six = "*" +certifi = "*" +urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""} + +[package.extras] +aiohttp = ["aiohttp (>=3.5)"] +beam = ["apache-beam (>=2.12)"] +bottle = ["bottle (>=0.12.13)"] +celery = ["celery (>=3)"] +chalice = ["chalice (>=1.16.0)"] +django = ["django (>=1.8)"] +falcon = ["falcon (>=1.4)"] +fastapi = ["fastapi (>=0.79.0)"] +flask = ["blinker (>=1.1)", "flask (>=0.11)"] +httpx = ["httpx (>=0.16.0)"] +huey = ["huey (>=2)"] +opentelemetry = ["opentelemetry-distro (>=0.35b0)"] +pure-eval = ["asttokens", "executing", "pure-eval"] +pymongo = ["pymongo (>=3.1)"] +pyspark = ["pyspark (>=2.4.4)"] +quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] +rq = ["rq (>=0.6)"] +sanic = ["sanic (>=0.8)"] +sqlalchemy = ["sqlalchemy (>=1.2)"] +starlette = ["starlette (>=0.19.1)"] +starlite = ["starlite (>=1.48)"] +tornado = ["tornado (>=5)"] + +[[package]] +name = "setuptools" +version = "67.4.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" +optional = false +python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "unittest2", "pytest-checkdocs (>=2.4)"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "singledispatch" +version = "4.0.0" +description = "Backport functools.singledispatch to older Pythons." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [[package]] name = "six" @@ -1792,21 +1848,21 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "snapshottest" -version = "0.5.1" -description = "Snapshot Testing utils for Python" +version = "0.6.0" +description = "Snapshot testing for pytest, unittest, Django, and Nose" category = "dev" optional = false python-versions = "*" [package.dependencies] -fastdiff = ">=0.1.4" +fastdiff = ">=0.1.4,<1" six = ">=1.10.0" termcolor = "*" [package.extras] nose = ["nose"] pytest = ["pytest"] -test = ["six", "pytest (>=3.1.0)", "pytest-cov", "nose", "django (>=1.10.6)"] +test = ["django (>=1.10.6)", "nose", "pytest (>=4.6)", "pytest-cov", "six"] [[package]] name = "sortedcontainers" @@ -1826,19 +1882,19 @@ python-versions = ">=3.5" [[package]] name = "stack-data" -version = "0.5.1" +version = "0.6.2" description = "Extract data from python stack frames and tracebacks for informative displays" category = "main" optional = false python-versions = "*" [package.dependencies] -asttokens = "*" -executing = "*" +asttokens = ">=2.1.0" +executing = ">=1.2.0" pure-eval = "*" [package.extras] -tests = ["cython", "littleutils", "pygments", "typeguard", "pytest"] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] [[package]] name = "tabula-py" @@ -1856,14 +1912,14 @@ requests = "*" [[package]] name = "termcolor" -version = "2.0.1" +version = "2.2.0" description = "ANSI color formatting for output in terminal" category = "dev" optional = false python-versions = ">=3.7" [package.extras] -tests = ["pytest-cov", "pytest"] +tests = ["pytest", "pytest-cov"] [[package]] name = "text-unidecode" @@ -1883,15 +1939,15 @@ python-versions = ">=3.7" [[package]] name = "traitlets" -version = "5.5.0" -description = "" +version = "5.9.0" +description = "Traitlets Python configuration system" category = "main" optional = false python-versions = ">=3.7" [package.extras] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["pre-commit", "pytest"] +test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] [[package]] name = "typing" @@ -1919,15 +1975,15 @@ python-versions = ">=3.6" [[package]] name = "urllib3" -version = "1.26.12" +version = "1.26.14" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.extras] -brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] @@ -1956,7 +2012,7 @@ python-versions = "*" [[package]] name = "wcwidth" -version = "0.2.5" +version = "0.2.6" description = "Measures the displayed width of unicode strings in a terminal" category = "main" optional = false @@ -1970,14 +2026,6 @@ category = "main" optional = false python-versions = "*" -[[package]] -name = "wrapt" -version = "1.14.1" -description = "Module for decorators, wrappers and monkey patching." -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - [[package]] name = "xhtml2pdf" version = "0.2.5" @@ -2006,26 +2054,41 @@ python-versions = "*" [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "51a9f2b9a88653ee4dbfeecd8eaa46050a185d67e85840c8c3cb85b710904f88" +content-hash = "aa77f0b51a1ea80f923f8997e7848f8ae26051135148b655ad69fb2b95f2e4b2" [metadata.files] amqp = [ {file = "amqp-5.1.1-py3-none-any.whl", hash = "sha256:6f0956d2c23d8fa6e7691934d8c3930eadb44972cbbd1a7ae3a520f735d43359"}, {file = "amqp-5.1.1.tar.gz", hash = "sha256:2c1b13fecc0893e946c65cbd5f36427861cffa4ea2201d8f6fca22e2a373b5e2"}, ] -aniso8601 = [] +aniso8601 = [ + {file = "aniso8601-7.0.0-py2.py3-none-any.whl", hash = "sha256:d10a4bf949f619f719b227ef5386e31f49a2b6d453004b21f02661ccc8670c7b"}, + {file = "aniso8601-7.0.0.tar.gz", hash = "sha256:513d2b6637b7853806ae79ffaca6f3e8754bdd547048f5ccc1420aec4b714f1e"}, +] appnope = [ {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, ] -arabic-reshaper = [] -asgiref = [] -asttokens = [] +arabic-reshaper = [ + {file = "arabic_reshaper-3.0.0-py3-none-any.whl", hash = "sha256:3f71d5034bb694204a239a6f1ebcf323ac3c5b059de02259235e2016a1a5e2dc"}, + {file = "arabic_reshaper-3.0.0.tar.gz", hash = "sha256:ffcd13ba5ec007db71c072f5b23f420da92ac7f268512065d49e790e62237099"}, +] +asgiref = [ + {file = "asgiref-3.6.0-py3-none-any.whl", hash = "sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac"}, + {file = "asgiref-3.6.0.tar.gz", hash = "sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506"}, +] +asttokens = [ + {file = "asttokens-2.2.1-py2.py3-none-any.whl", hash = "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c"}, + {file = "asttokens-2.2.1.tar.gz", hash = "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3"}, +] async-timeout = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, ] -attrs = [] +attrs = [ + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, +] azure-common = [ {file = "azure-common-1.1.19.zip", hash = "sha256:622d9360a1b61172b4c0d1cc58f939c68402aa19ca44872ab3d224d913aa6d0c"}, {file = "azure_common-1.1.19-py2.py3-none-any.whl", hash = "sha256:14722caf6c3ed81d2cfdd3e448635fdc78f214dc6f17558dd1ca5b87bccc0631"}, @@ -2075,16 +2138,96 @@ botocore = [ {file = "botocore-1.23.54-py3-none-any.whl", hash = "sha256:06ae8076c4dcf3d72bec4d37e5f2dce4a92a18a8cdaa3bfaa6e3b7b5e30a8d7e"}, {file = "botocore-1.23.54.tar.gz", hash = "sha256:4bb9ba16cccee5f5a2602049bc3e2db6865346b2550667f3013bdf33b0a01ceb"}, ] -cachetools = [] +cachetools = [ + {file = "cachetools-5.3.0-py3-none-any.whl", hash = "sha256:429e1a1e845c008ea6c85aa35d4b98b65d6a9763eeef3e37e92728a12d1de9d4"}, + {file = "cachetools-5.3.0.tar.gz", hash = "sha256:13dfddc7b8df938c21a940dfa6557ce6e94a2f1cdfa58eb90c805721d58f2c14"}, +] celery = [ {file = "celery-5.1.2-py3-none-any.whl", hash = "sha256:9dab2170b4038f7bf10ef2861dbf486ddf1d20592290a1040f7b7a1259705d42"}, {file = "celery-5.1.2.tar.gz", hash = "sha256:8d9a3de9162965e97f8e8cc584c67aad83b3f7a267584fa47701ed11c3e0d4b0"}, ] -certifi = [] -cffi = [] -chardet = [] -charset-normalizer = [] -choicesenum = [] +certifi = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] +cffi = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] +chardet = [ + {file = "chardet-5.1.0-py3-none-any.whl", hash = "sha256:362777fb014af596ad31334fde1e8c327dfdb076e1960d1694662d46a6917ab9"}, + {file = "chardet-5.1.0.tar.gz", hash = "sha256:0d62712b956bc154f85fb0a266e2a3c5913c2967e00348701b32411d6def31e5"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, +] +choicesenum = [ + {file = "choicesenum-0.7.0-py2.py3-none-any.whl", hash = "sha256:8b8c1f8a374f537441303992009907234c36a587d3a93d248c878c2f104a2b7d"}, + {file = "choicesenum-0.7.0.tar.gz", hash = "sha256:37d53174a66405ff178ac44396be9f3a71fe8f5b43d3a5a6ebfaa9593543d36a"}, +] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, @@ -2105,7 +2248,10 @@ cligj = [ {file = "cligj-0.7.2-py3-none-any.whl", hash = "sha256:c1ca117dbce1fe20a5809dc96f01e1c2840f6dcc939b3ddbb1111bf330ba82df"}, {file = "cligj-0.7.2.tar.gz", hash = "sha256:a4bc13d623356b373c2c27c53dbd9c68cae5d526270bfa71f6c6fa69669c6b27"}, ] -colorama = [] +colorama = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] coreapi = [ {file = "coreapi-2.3.3-py2.py3-none-any.whl", hash = "sha256:bf39d118d6d3e171f10df9ede5666f63ad80bba9a29a8ec17726a66cf52ee6f3"}, {file = "coreapi-2.3.3.tar.gz", hash = "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb"}, @@ -2147,26 +2293,44 @@ coverage = [ {file = "coverage-4.4.2-cp36-cp36m-win32.whl", hash = "sha256:f87f522bde5540d8a4b11df80058281ac38c44b13ce29ced1e294963dd51a8f8"}, {file = "coverage-4.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a7cfaebd8f24c2b537fa6a271229b051cdac9c1734bb6f939ccfc7c055689baa"}, {file = "coverage-4.4.2.tar.gz", hash = "sha256:309d91bd7a35063ec7a0e4d75645488bfab3f0b66373e7722f23da7f5b0f34cc"}, - {file = "coverage-4.4.2.win-amd64-py2.7.exe", hash = "sha256:b6cebae1502ce5b87d7c6f532fa90ab345cfbda62b95aeea4e431e164d498a3d"}, - {file = "coverage-4.4.2.win-amd64-py3.4.exe", hash = "sha256:a4497faa4f1c0fc365ba05eaecfb6b5d24e3c8c72e95938f9524e29dadb15e76"}, - {file = "coverage-4.4.2.win-amd64-py3.5.exe", hash = "sha256:2b4d7f03a8a6632598cbc5df15bbca9f778c43db7cf1a838f4fa2c8599a8691a"}, - {file = "coverage-4.4.2.win-amd64-py3.6.exe", hash = "sha256:1afccd7e27cac1b9617be8c769f6d8a6d363699c9b86820f40c74cfb3328921c"}, - {file = "coverage-4.4.2.win32-py2.7.exe", hash = "sha256:0388c12539372bb92d6dde68b4627f0300d948965bbb7fc104924d715fdc0965"}, - {file = "coverage-4.4.2.win32-py3.4.exe", hash = "sha256:ab3508df9a92c1d3362343d235420d08e2662969b83134f8a97dc1451cbe5e84"}, - {file = "coverage-4.4.2.win32-py3.5.exe", hash = "sha256:43a155eb76025c61fc20c3d03b89ca28efa6f5be572ab6110b2fb68eda96bfea"}, - {file = "coverage-4.4.2.win32-py3.6.exe", hash = "sha256:f98b461cb59f117887aa634a66022c0bd394278245ed51189f63a036516e32de"}, -] -cryptography = [] +] +cryptography = [ + {file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:6687ef6d0a6497e2b58e7c5b852b53f62142cfa7cd1555795758934da363a965"}, + {file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:706843b48f9a3f9b9911979761c91541e3d90db1ca905fd63fee540a217698bc"}, + {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5d2d8b87a490bfcd407ed9d49093793d0f75198a35e6eb1a923ce1ee86c62b41"}, + {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e17b26de248c33f3acffb922748151d71827d6021d98c70e6c1a25ddd78505"}, + {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e124352fd3db36a9d4a21c1aa27fd5d051e621845cb87fb851c08f4f75ce8be6"}, + {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:5aa67414fcdfa22cf052e640cb5ddc461924a045cacf325cd164e65312d99502"}, + {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:35f7c7d015d474f4011e859e93e789c87d21f6f4880ebdc29896a60403328f1f"}, + {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f24077a3b5298a5a06a8e0536e3ea9ec60e4c7ac486755e5fb6e6ea9b3500106"}, + {file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f0c64d1bd842ca2633e74a1a28033d139368ad959872533b1bab8c80e8240a0c"}, + {file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0f8da300b5c8af9f98111ffd512910bc792b4c77392a9523624680f7956a99d4"}, + {file = "cryptography-39.0.1-cp36-abi3-win32.whl", hash = "sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8"}, + {file = "cryptography-39.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac"}, + {file = "cryptography-39.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad"}, + {file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5caeb8188c24888c90b5108a441c106f7faa4c4c075a2bcae438c6e8ca73cef"}, + {file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4789d1e3e257965e960232345002262ede4d094d1a19f4d3b52e48d4d8f3b885"}, + {file = "cryptography-39.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388"}, + {file = "cryptography-39.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336"}, + {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2"}, + {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:6f8ba7f0328b79f08bdacc3e4e66fb4d7aab0c3584e0bd41328dce5262e26b2e"}, + {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ef8b72fa70b348724ff1218267e7f7375b8de4e8194d1636ee60510aae104cd0"}, + {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:aec5a6c9864be7df2240c382740fcf3b96928c46604eaa7f3091f58b878c0bb6"}, + {file = "cryptography-39.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdd188c8a6ef8769f148f88f859884507b954cc64db6b52f66ef199bb9ad660a"}, + {file = "cryptography-39.0.1.tar.gz", hash = "sha256:d1f6198ee6d9148405e49887803907fe8962a23e6c6f83ea7d98f1c0de375695"}, +] decorator = [ {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] -deprecated = [ - {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, - {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, +distro = [ + {file = "distro-1.8.0-py3-none-any.whl", hash = "sha256:99522ca3e365cac527b44bde033f64c6945d90eb9f769703caaec52b09bbd3ff"}, + {file = "distro-1.8.0.tar.gz", hash = "sha256:02e111d1dc6a50abb8eed6bf31c3e48ed8b0830d1ea2a1b78c61765c2513fdd8"}, +] +django = [ + {file = "Django-3.2.18-py3-none-any.whl", hash = "sha256:4d492d9024c7b3dfababf49f94511ab6a58e2c9c3c7207786f1ba4eb77750706"}, + {file = "Django-3.2.18.tar.gz", hash = "sha256:08208dfe892eb64fff073ca743b3b952311104f939e7f6dae954fe72dcc533ba"}, ] -distro = [] -django = [] django-admin-autocomplete-filter = [ {file = "django-admin-autocomplete-filter-0.5.tar.gz", hash = "sha256:9d95a4407c3289e823c3486ddb200124c4ec8edf11064a0b8cb998d32c39530e"}, {file = "django_admin_autocomplete_filter-0.5-py3-none-any.whl", hash = "sha256:44556b4b4d02c09a7ccdfc43e38c1e0f032741c6d484e9a6c542d7180b029418"}, @@ -2175,15 +2339,21 @@ django-admin-list-filter-dropdown = [ {file = "django-admin-list-filter-dropdown-1.0.2.tar.gz", hash = "sha256:e866c4a5c100a89ab41ff4f2c224e2104a63fd8e877366defa1a00516db0c57a"}, {file = "django_admin_list_filter_dropdown-1.0.2-py3-none-any.whl", hash = "sha256:732496b9fcf68d0547a96ace0a7cd71a370cd7fe6c78e9cc0e3749bdf2861d89"}, ] -django-cors-headers = [] +django-cors-headers = [ + {file = "django-cors-headers-3.11.0.tar.gz", hash = "sha256:eb98389bf7a2afc5d374806af4a9149697e3a6955b5a2dc2bf049f7d33647456"}, + {file = "django_cors_headers-3.11.0-py3-none-any.whl", hash = "sha256:a22be2befd4069c4fc174f11cf067351df5c061a3a5f94a01650b4e928b0372b"}, +] django-coverage = [ {file = "django-coverage-1.2.4.tar.gz", hash = "sha256:eee56c1465b2ece0a066ea2514c50039462f8fe1ea58e59adc0dfda14b30628b"}, ] django-debug-toolbar = [ - {file = "django-debug-toolbar-2.2.1.tar.gz", hash = "sha256:7aadab5240796ffe8e93cc7dfbe2f87a204054746ff7ff93cd6d4a0c3747c853"}, - {file = "django_debug_toolbar-2.2.1-py3-none-any.whl", hash = "sha256:7feaee934608f5cdd95432154be832fe30fda6c1249018191e2c27bc0b6a965e"}, + {file = "django_debug_toolbar-3.8.1-py3-none-any.whl", hash = "sha256:879f8a4672d41621c06a4d322dcffa630fc4df056cada6e417ed01db0e5e0478"}, + {file = "django_debug_toolbar-3.8.1.tar.gz", hash = "sha256:24ef1a7d44d25e60d7951e378454c6509bf536dce7e7d9d36e7c387db499bc27"}, +] +django-enumfield = [ + {file = "django-enumfield-2.0.2.tar.gz", hash = "sha256:e5fe9826b5f6c5372c70ab82d9afa126fac932a88af2ae46b530b9ef520b1c8f"}, + {file = "django_enumfield-2.0.2-py2.py3-none-any.whl", hash = "sha256:4a237885151bf36b94f084cdb652fda6dcf1b2d3796b8d31764c33d30cfd478f"}, ] -django-enumfield = [] django-environ = [ {file = "django-environ-0.8.1.tar.gz", hash = "sha256:6f0bc902b43891656b20486938cba0861dc62892784a44919170719572a534cb"}, {file = "django_environ-0.8.1-py2.py3-none-any.whl", hash = "sha256:42593bee519a527602a467c7b682aee1a051c2597f98c45f4f4f44169ecdb6e5"}, @@ -2196,8 +2366,16 @@ django-filter = [ {file = "django-filter-2.4.0.tar.gz", hash = "sha256:84e9d5bb93f237e451db814ed422a3a625751cbc9968b484ecc74964a8696b06"}, {file = "django_filter-2.4.0-py3-none-any.whl", hash = "sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1"}, ] -django-guardian = [] -django-modeltranslation = [] +django-guardian = [ + {file = "django-guardian-2.4.0.tar.gz", hash = "sha256:c58a68ae76922d33e6bdc0e69af1892097838de56e93e78a8361090bcd9f89a0"}, + {file = "django_guardian-2.4.0-py3-none-any.whl", hash = "sha256:440ca61358427e575323648b25f8384739e54c38b3d655c81d75e0cd0d61b697"}, +] +django-haystack = [ + {file = "django-haystack-3.2.1.tar.gz", hash = "sha256:97e3197aefc225fe405b6f17600a2534bf827cb4d6743130c20bc1a06f7293a4"}, +] +django-modeltranslation = [ + {file = "django-modeltranslation-0.17.5.tar.gz", hash = "sha256:d9b3278dc3a799261861e090c27a3e1c46b3471e979e07b01cdd4ea2696a9f41"}, +] django-redis = [ {file = "django-redis-5.0.0.tar.gz", hash = "sha256:048f665bbe27f8ff2edebae6aa9c534ab137f1e8fa7234147ef470df3f3aa9b8"}, {file = "django_redis-5.0.0-py3-none-any.whl", hash = "sha256:97739ca9de3f964c51412d1d7d8aecdfd86737bb197fce6e1ff12620c63c97ee"}, @@ -2209,9 +2387,11 @@ django-reversion = [ django-reversion-compare = [ {file = "django-reversion-compare-0.9.0.tar.gz", hash = "sha256:bd68557fcf94c6501cdb3377e9d4b24bad0779422bdf7a82f51a0c71cbbe1313"}, {file = "django_reversion_compare-0.9.0-py2.py3-none-any.whl", hash = "sha256:269ec0ecde8d2f020b1945a87a92f05a7871680d1c2bfbdb9d6dc79825c84a41"}, - {file = "django_reversion_compare-0.9.0-py3.6.egg", hash = "sha256:19e10c38d3aba922bcfacc9f69b1127756e8cac47ce7b1f5c0e22d0d090c0f42"}, ] -django-storages = [] +django-storages = [ + {file = "django-storages-1.11.1.tar.gz", hash = "sha256:c823dbf56c9e35b0999a13d7e05062b837bae36c518a40255d522fbe3750fbb4"}, + {file = "django_storages-1.11.1-py3-none-any.whl", hash = "sha256:f28765826d507a0309cfaa849bd084894bc71d81bf0d09479168d44785396f80"}, +] django-tinymce4-lite = [ {file = "django-tinymce4-lite-1.7.4.tar.gz", hash = "sha256:90bef25a20f43b57040ad631605de3bcccccc7ac760c42640edbf459062af8d2"}, {file = "django_tinymce4_lite-1.7.4-py2.py3-none-any.whl", hash = "sha256:cf4a41abc704e56515eb48d9d8fd3c56ceeb9705419bdff2b8a375e2131524f8"}, @@ -2220,7 +2400,9 @@ djangorestframework = [ {file = "djangorestframework-3.11.2-py3-none-any.whl", hash = "sha256:5cc724dc4b076463497837269107e1995b1fbc917468d1b92d188fd1af9ea789"}, {file = "djangorestframework-3.11.2.tar.gz", hash = "sha256:a5967b68a04e0d97d10f4df228e30f5a2d82ba63b9d03e1759f84993b7bf1b53"}, ] -djangorestframework-camel-case = [] +djangorestframework-camel-case = [ + {file = "djangorestframework-camel-case-1.2.0.tar.gz", hash = "sha256:9714d43fba5bb654057c29501649684d3d9f11a92319ae417fd4d65e80d1159d"}, +] djangorestframework-csv = [ {file = "djangorestframework-csv-2.1.1.tar.gz", hash = "sha256:aa0ee4c894fe319c68e042b05c61dace43a9fb6e6872e1abe1724ca7ea4d15f7"}, ] @@ -2228,34 +2410,69 @@ djangorestframework-guardian = [ {file = "djangorestframework_guardian-0.1.1-py2.py3-none-any.whl", hash = "sha256:32d723a5c62f75b72c618312358b1c186ec1c1fa95ffc2169e125df62ef20015"}, ] elasticsearch = [ - {file = "elasticsearch-6.8.2-py2.py3-none-any.whl", hash = "sha256:1aedf00b73f5d1e77cb4df70fec58f2efb664be4ce2686374239aa6c0373c65c"}, - {file = "elasticsearch-6.8.2.tar.gz", hash = "sha256:c3a560bb83e4981b5a5c82080d2ceb99686d33692ef53365656129478aa5ddb2"}, + {file = "elasticsearch-7.0.0-py2.py3-none-any.whl", hash = "sha256:c621f2272bb2f000d228dc7016d058a1f90f15babdc938240b9f2d13690222db"}, + {file = "elasticsearch-7.0.0.tar.gz", hash = "sha256:cf6cf834b6d0172dac5e704c398a11d1917cf61f15d32b79b1ddad4cd673c4b1"}, +] +et-xmlfile = [ + {file = "et_xmlfile-1.1.0-py3-none-any.whl", hash = "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada"}, + {file = "et_xmlfile-1.1.0.tar.gz", hash = "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c"}, +] +exceptiongroup = [ + {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, + {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, +] +executing = [ + {file = "executing-1.2.0-py2.py3-none-any.whl", hash = "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc"}, + {file = "executing-1.2.0.tar.gz", hash = "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"}, ] -executing = [] factory-boy = [ {file = "factory_boy-2.12.0-py2.py3-none-any.whl", hash = "sha256:728df59b372c9588b83153facf26d3d28947fc750e8e3c95cefa9bed0e6394ee"}, {file = "factory_boy-2.12.0.tar.gz", hash = "sha256:faf48d608a1735f0d0a3c9cbf536d64f9132b547dae7ba452c4d99a79e84a370"}, ] -faker = [] +faker = [ + {file = "Faker-17.0.0-py3-none-any.whl", hash = "sha256:21c3c6c45183308151c14f62afe59bf54ace68f663e0180973698ba2a9a3b2c4"}, + {file = "Faker-17.0.0.tar.gz", hash = "sha256:17cf85aeb0363a3384ccd4c1f52b52ec8f414c7afaab74ae1f4c3e09a06e14de"}, +] fastdiff = [ {file = "fastdiff-0.3.0-py2.py3-none-any.whl", hash = "sha256:ca5f61f6ddf5a1564ddfd98132ad28e7abe4a88a638a8b014a2214f71e5918ec"}, {file = "fastdiff-0.3.0.tar.gz", hash = "sha256:4dfa09c47832a8c040acda3f1f55fc0ab4d666f0e14e6951e6da78d59acd945a"}, ] -future = [ - {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, -] fuzzywuzzy = [ {file = "fuzzywuzzy-0.17.0-py2.py3-none-any.whl", hash = "sha256:5ac7c0b3f4658d2743aa17da53a55598144edbc5bee3c6863840636e6926f254"}, {file = "fuzzywuzzy-0.17.0.tar.gz", hash = "sha256:6f49de47db00e1c71d40ad16da42284ac357936fa9b66bea1df63fed07122d62"}, ] -google-api-core = [] -google-auth = [] -googleapis-common-protos = [] -gprof2dot = [] -graphene = [] -graphene-django = [] -graphql-core = [] -graphql-relay = [] +google-api-core = [ + {file = "google-api-core-2.11.0.tar.gz", hash = "sha256:4b9bb5d5a380a0befa0573b302651b8a9a89262c1730e37bf423cec511804c22"}, + {file = "google_api_core-2.11.0-py3-none-any.whl", hash = "sha256:ce222e27b0de0d7bc63eb043b956996d6dccab14cc3b690aaea91c9cc99dc16e"}, +] +google-auth = [ + {file = "google-auth-2.16.1.tar.gz", hash = "sha256:5fd170986bce6bfd7bb5c845c4b8362edb1e0cba901e062196e83f8bb5d5d32c"}, + {file = "google_auth-2.16.1-py2.py3-none-any.whl", hash = "sha256:75d76ea857df65938e1f71dcbcd7d0cd48e3f80b34b8870ba229c9292081f7ef"}, +] +googleapis-common-protos = [ + {file = "googleapis-common-protos-1.58.0.tar.gz", hash = "sha256:c727251ec025947d545184ba17e3578840fc3a24a0516a020479edab660457df"}, + {file = "googleapis_common_protos-1.58.0-py2.py3-none-any.whl", hash = "sha256:ca3befcd4580dab6ad49356b46bf165bb68ff4b32389f028f1abd7c10ab9519a"}, +] +gprof2dot = [ + {file = "gprof2dot-2022.7.29-py2.py3-none-any.whl", hash = "sha256:f165b3851d3c52ee4915eb1bd6cca571e5759823c2cd0f71a79bda93c2dc85d6"}, + {file = "gprof2dot-2022.7.29.tar.gz", hash = "sha256:45b4d298bd36608fccf9511c3fd88a773f7a1abc04d6cd39445b11ba43133ec5"}, +] +graphene = [ + {file = "graphene-2.1.9-py2.py3-none-any.whl", hash = "sha256:3d446eb1237c551052bc31155cf1a3a607053e4f58c9172b83a1b597beaa0868"}, + {file = "graphene-2.1.9.tar.gz", hash = "sha256:b9f2850e064eebfee9a3ef4a1f8aa0742848d97652173ab44c82cc8a62b9ed93"}, +] +graphene-django = [ + {file = "graphene-django-2.15.0.tar.gz", hash = "sha256:b78c9b05bc899016b9cc5bf13faa1f37fe1faa8c5407552c6ddd1a28f46fc31a"}, + {file = "graphene_django-2.15.0-py2.py3-none-any.whl", hash = "sha256:02671d195f0c09c8649acff2a8f4ad4f297d0f7d98ea6e6cdf034b81bab92880"}, +] +graphql-core = [ + {file = "graphql-core-2.3.2.tar.gz", hash = "sha256:aac46a9ac524c9855910c14c48fc5d60474def7f99fd10245e76608eba7af746"}, + {file = "graphql_core-2.3.2-py2.py3-none-any.whl", hash = "sha256:44c9bac4514e5e30c5a595fac8e3c76c1975cae14db215e8174c7fe995825bad"}, +] +graphql-relay = [ + {file = "graphql-relay-2.0.1.tar.gz", hash = "sha256:870b6b5304123a38a0b215a79eace021acce5a466bf40cd39fa18cb8528afabb"}, + {file = "graphql_relay-2.0.1-py3-none-any.whl", hash = "sha256:ac514cb86db9a43014d7e73511d521137ac12cf0101b2eaa5f0a3da2e10d913d"}, +] gunicorn = [ {file = "gunicorn-19.7.1-py2.py3-none-any.whl", hash = "sha256:75af03c99389535f218cc596c7de74df4763803f7b63eb09d77e92b3956b36c6"}, {file = "gunicorn-19.7.1.tar.gz", hash = "sha256:eee1169f0ca667be05db3351a0960765620dad53f53434262ff8901b68a1b622"}, @@ -2264,21 +2481,30 @@ html5lib = [ {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, ] -idna = [] +idna = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] +ipython = [ + {file = "ipython-8.10.0-py3-none-any.whl", hash = "sha256:b38c31e8fc7eff642fc7c597061fff462537cf2314e3225a19c906b7b0d8a345"}, + {file = "ipython-8.10.0.tar.gz", hash = "sha256:b13a1d6c1f5818bd388db53b7107d17454129a70de2b87481d555daede5eb49e"}, ] -ipython = [] itypes = [ {file = "itypes-1.2.0-py2.py3-none-any.whl", hash = "sha256:03da6872ca89d29aef62773672b2d408f490f80db48b23079a4b194c86dd04c6"}, {file = "itypes-1.2.0.tar.gz", hash = "sha256:af886f129dea4a2a1e3d36595a2d139589e4dd287f5cab0b40e799ee81570ff1"}, ] jedi = [ - {file = "jedi-0.18.1-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"}, - {file = "jedi-0.18.1.tar.gz", hash = "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"}, + {file = "jedi-0.18.2-py2.py3-none-any.whl", hash = "sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e"}, + {file = "jedi-0.18.2.tar.gz", hash = "sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612"}, +] +jinja2 = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] -jinja2 = [] jmespath = [ {file = "jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"}, {file = "jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9"}, @@ -2298,7 +2524,78 @@ kombu = [ {file = "kombu-5.2.4-py3-none-any.whl", hash = "sha256:8b213b24293d3417bcf0d2f5537b7f756079e3ea232a8386dcc89a59fd2361a4"}, {file = "kombu-5.2.4.tar.gz", hash = "sha256:37cee3ee725f94ea8bb173eaab7c1760203ea53bbebae226328600f9d2799610"}, ] -lxml = [] +lxml = [ + {file = "lxml-4.9.1-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:98cafc618614d72b02185ac583c6f7796202062c41d2eeecdf07820bad3295ed"}, + {file = "lxml-4.9.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c62e8dd9754b7debda0c5ba59d34509c4688f853588d75b53c3791983faa96fc"}, + {file = "lxml-4.9.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:21fb3d24ab430fc538a96e9fbb9b150029914805d551deeac7d7822f64631dfc"}, + {file = "lxml-4.9.1-cp27-cp27m-win32.whl", hash = "sha256:86e92728ef3fc842c50a5cb1d5ba2bc66db7da08a7af53fb3da79e202d1b2cd3"}, + {file = "lxml-4.9.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4cfbe42c686f33944e12f45a27d25a492cc0e43e1dc1da5d6a87cbcaf2e95627"}, + {file = "lxml-4.9.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dad7b164905d3e534883281c050180afcf1e230c3d4a54e8038aa5cfcf312b84"}, + {file = "lxml-4.9.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a614e4afed58c14254e67862456d212c4dcceebab2eaa44d627c2ca04bf86837"}, + {file = "lxml-4.9.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f9ced82717c7ec65a67667bb05865ffe38af0e835cdd78728f1209c8fffe0cad"}, + {file = "lxml-4.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:d9fc0bf3ff86c17348dfc5d322f627d78273eba545db865c3cd14b3f19e57fa5"}, + {file = "lxml-4.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e5f66bdf0976ec667fc4594d2812a00b07ed14d1b44259d19a41ae3fff99f2b8"}, + {file = "lxml-4.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fe17d10b97fdf58155f858606bddb4e037b805a60ae023c009f760d8361a4eb8"}, + {file = "lxml-4.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8caf4d16b31961e964c62194ea3e26a0e9561cdf72eecb1781458b67ec83423d"}, + {file = "lxml-4.9.1-cp310-cp310-win32.whl", hash = "sha256:4780677767dd52b99f0af1f123bc2c22873d30b474aa0e2fc3fe5e02217687c7"}, + {file = "lxml-4.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:b122a188cd292c4d2fcd78d04f863b789ef43aa129b233d7c9004de08693728b"}, + {file = "lxml-4.9.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:be9eb06489bc975c38706902cbc6888f39e946b81383abc2838d186f0e8b6a9d"}, + {file = "lxml-4.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f1be258c4d3dc609e654a1dc59d37b17d7fef05df912c01fc2e15eb43a9735f3"}, + {file = "lxml-4.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:927a9dd016d6033bc12e0bf5dee1dde140235fc8d0d51099353c76081c03dc29"}, + {file = "lxml-4.9.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9232b09f5efee6a495a99ae6824881940d6447debe272ea400c02e3b68aad85d"}, + {file = "lxml-4.9.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:04da965dfebb5dac2619cb90fcf93efdb35b3c6994fea58a157a834f2f94b318"}, + {file = "lxml-4.9.1-cp35-cp35m-win32.whl", hash = "sha256:4d5bae0a37af799207140652a700f21a85946f107a199bcb06720b13a4f1f0b7"}, + {file = "lxml-4.9.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4878e667ebabe9b65e785ac8da4d48886fe81193a84bbe49f12acff8f7a383a4"}, + {file = "lxml-4.9.1-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:1355755b62c28950f9ce123c7a41460ed9743c699905cbe664a5bcc5c9c7c7fb"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:bcaa1c495ce623966d9fc8a187da80082334236a2a1c7e141763ffaf7a405067"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6eafc048ea3f1b3c136c71a86db393be36b5b3d9c87b1c25204e7d397cee9536"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:13c90064b224e10c14dcdf8086688d3f0e612db53766e7478d7754703295c7c8"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206a51077773c6c5d2ce1991327cda719063a47adc02bd703c56a662cdb6c58b"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e8f0c9d65da595cfe91713bc1222af9ecabd37971762cb830dea2fc3b3bb2acf"}, + {file = "lxml-4.9.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8f0a4d179c9a941eb80c3a63cdb495e539e064f8054230844dcf2fcb812b71d3"}, + {file = "lxml-4.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:830c88747dce8a3e7525defa68afd742b4580df6aa2fdd6f0855481e3994d391"}, + {file = "lxml-4.9.1-cp36-cp36m-win32.whl", hash = "sha256:1e1cf47774373777936c5aabad489fef7b1c087dcd1f426b621fda9dcc12994e"}, + {file = "lxml-4.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:5974895115737a74a00b321e339b9c3f45c20275d226398ae79ac008d908bff7"}, + {file = "lxml-4.9.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:1423631e3d51008871299525b541413c9b6c6423593e89f9c4cfbe8460afc0a2"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:2aaf6a0a6465d39b5ca69688fce82d20088c1838534982996ec46633dc7ad6cc"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:9f36de4cd0c262dd9927886cc2305aa3f2210db437aa4fed3fb4940b8bf4592c"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae06c1e4bc60ee076292e582a7512f304abdf6c70db59b56745cca1684f875a4"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:57e4d637258703d14171b54203fd6822fda218c6c2658a7d30816b10995f29f3"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6d279033bf614953c3fc4a0aa9ac33a21e8044ca72d4fa8b9273fe75359d5cca"}, + {file = "lxml-4.9.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a60f90bba4c37962cbf210f0188ecca87daafdf60271f4c6948606e4dabf8785"}, + {file = "lxml-4.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6ca2264f341dd81e41f3fffecec6e446aa2121e0b8d026fb5130e02de1402785"}, + {file = "lxml-4.9.1-cp37-cp37m-win32.whl", hash = "sha256:27e590352c76156f50f538dbcebd1925317a0f70540f7dc8c97d2931c595783a"}, + {file = "lxml-4.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:eea5d6443b093e1545ad0210e6cf27f920482bfcf5c77cdc8596aec73523bb7e"}, + {file = "lxml-4.9.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f05251bbc2145349b8d0b77c0d4e5f3b228418807b1ee27cefb11f69ed3d233b"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:487c8e61d7acc50b8be82bda8c8d21d20e133c3cbf41bd8ad7eb1aaeb3f07c97"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d1a92d8e90b286d491e5626af53afef2ba04da33e82e30744795c71880eaa21"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:b570da8cd0012f4af9fa76a5635cd31f707473e65a5a335b186069d5c7121ff2"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ef87fca280fb15342726bd5f980f6faf8b84a5287fcc2d4962ea8af88b35130"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:93e414e3206779ef41e5ff2448067213febf260ba747fc65389a3ddaa3fb8715"}, + {file = "lxml-4.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6653071f4f9bac46fbc30f3c7838b0e9063ee335908c5d61fb7a4a86c8fd2036"}, + {file = "lxml-4.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:32a73c53783becdb7eaf75a2a1525ea8e49379fb7248c3eeefb9412123536387"}, + {file = "lxml-4.9.1-cp38-cp38-win32.whl", hash = "sha256:1a7c59c6ffd6ef5db362b798f350e24ab2cfa5700d53ac6681918f314a4d3b94"}, + {file = "lxml-4.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:1436cf0063bba7888e43f1ba8d58824f085410ea2025befe81150aceb123e345"}, + {file = "lxml-4.9.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:4beea0f31491bc086991b97517b9683e5cfb369205dac0148ef685ac12a20a67"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:41fb58868b816c202e8881fd0f179a4644ce6e7cbbb248ef0283a34b73ec73bb"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bd34f6d1810d9354dc7e35158aa6cc33456be7706df4420819af6ed966e85448"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:edffbe3c510d8f4bf8640e02ca019e48a9b72357318383ca60e3330c23aaffc7"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d949f53ad4fc7cf02c44d6678e7ff05ec5f5552b235b9e136bd52e9bf730b91"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:079b68f197c796e42aa80b1f739f058dcee796dc725cc9a1be0cdb08fc45b000"}, + {file = "lxml-4.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9c3a88d20e4fe4a2a4a84bf439a5ac9c9aba400b85244c63a1ab7088f85d9d25"}, + {file = "lxml-4.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4e285b5f2bf321fc0857b491b5028c5f276ec0c873b985d58d7748ece1d770dd"}, + {file = "lxml-4.9.1-cp39-cp39-win32.whl", hash = "sha256:ef72013e20dd5ba86a8ae1aed7f56f31d3374189aa8b433e7b12ad182c0d2dfb"}, + {file = "lxml-4.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:10d2017f9150248563bb579cd0d07c61c58da85c922b780060dcc9a3aa9f432d"}, + {file = "lxml-4.9.1-pp37-pypy37_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538747a9d7827ce3e16a8fdd201a99e661c7dee3c96c885d8ecba3c35d1032c"}, + {file = "lxml-4.9.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0645e934e940107e2fdbe7c5b6fb8ec6232444260752598bc4d09511bd056c0b"}, + {file = "lxml-4.9.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6daa662aba22ef3258934105be2dd9afa5bb45748f4f702a3b39a5bf53a1f4dc"}, + {file = "lxml-4.9.1-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:603a464c2e67d8a546ddaa206d98e3246e5db05594b97db844c2f0a1af37cf5b"}, + {file = "lxml-4.9.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c4b2e0559b68455c085fb0f6178e9752c4be3bba104d6e881eb5573b399d1eb2"}, + {file = "lxml-4.9.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0f3f0059891d3254c7b5fb935330d6db38d6519ecd238ca4fce93c234b4a0f73"}, + {file = "lxml-4.9.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c852b1530083a620cb0de5f3cd6826f19862bafeaf77586f1aef326e49d95f0c"}, + {file = "lxml-4.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:287605bede6bd36e930577c5925fcea17cb30453d96a7b4c63c14a257118dbb9"}, + {file = "lxml-4.9.1.tar.gz", hash = "sha256:fe749b052bb7233fe5d072fcb549221a8cb1a16725c47c37e42b0b9cb3ff2c3f"}, +] mapbox-tilesets = [ {file = "mapbox-tilesets-1.7.3.tar.gz", hash = "sha256:ac71370293ab1895cd8bfa333d808f45f13313a99c5378fa82c8427d45e5f57c"}, {file = "mapbox_tilesets-1.7.3-py3-none-any.whl", hash = "sha256:c2881fa302c7b2877b024efcb8a0f98577fe718d3e3c4e85c64c68644877a6d6"}, @@ -2308,55 +2605,103 @@ markdown = [ {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, ] markupsafe = [ - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, - {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, -] -matplotlib-inline = [] + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, + {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, +] +matplotlib-inline = [ + {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, + {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, +] mercantile = [ {file = "mercantile-1.1.6-py3-none-any.whl", hash = "sha256:20895bcee8cb346f384d8a1161fde4773f5b2aa937d90a23aebde766a0d1a536"}, {file = "mercantile-1.1.6.tar.gz", hash = "sha256:0dff4cbc2c92ceca0e0dfbb3dc74392a96d33cfa29afb1bdfcc80283d3ef4207"}, ] -numpy = [] -opencensus = [] -opencensus-context = [] +numpy = [ + {file = "numpy-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eef70b4fc1e872ebddc38cddacc87c19a3709c0e3e5d20bf3954c147b1dd941d"}, + {file = "numpy-1.24.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8d2859428712785e8a8b7d2b3ef0a1d1565892367b32f915c4a4df44d0e64f5"}, + {file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6524630f71631be2dabe0c541e7675db82651eb998496bbe16bc4f77f0772253"}, + {file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a51725a815a6188c662fb66fb32077709a9ca38053f0274640293a14fdd22978"}, + {file = "numpy-1.24.2-cp310-cp310-win32.whl", hash = "sha256:2620e8592136e073bd12ee4536149380695fbe9ebeae845b81237f986479ffc9"}, + {file = "numpy-1.24.2-cp310-cp310-win_amd64.whl", hash = "sha256:97cf27e51fa078078c649a51d7ade3c92d9e709ba2bfb97493007103c741f1d0"}, + {file = "numpy-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7de8fdde0003f4294655aa5d5f0a89c26b9f22c0a58790c38fae1ed392d44a5a"}, + {file = "numpy-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4173bde9fa2a005c2c6e2ea8ac1618e2ed2c1c6ec8a7657237854d42094123a0"}, + {file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cecaed30dc14123020f77b03601559fff3e6cd0c048f8b5289f4eeabb0eb281"}, + {file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a23f8440561a633204a67fb44617ce2a299beecf3295f0d13c495518908e910"}, + {file = "numpy-1.24.2-cp311-cp311-win32.whl", hash = "sha256:e428c4fbfa085f947b536706a2fc349245d7baa8334f0c5723c56a10595f9b95"}, + {file = "numpy-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:557d42778a6869c2162deb40ad82612645e21d79e11c1dc62c6e82a2220ffb04"}, + {file = "numpy-1.24.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d0a2db9d20117bf523dde15858398e7c0858aadca7c0f088ac0d6edd360e9ad2"}, + {file = "numpy-1.24.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c72a6b2f4af1adfe193f7beb91ddf708ff867a3f977ef2ec53c0ffb8283ab9f5"}, + {file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29e6bd0ec49a44d7690ecb623a8eac5ab8a923bce0bea6293953992edf3a76a"}, + {file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eabd64ddb96a1239791da78fa5f4e1693ae2dadc82a76bc76a14cbb2b966e96"}, + {file = "numpy-1.24.2-cp38-cp38-win32.whl", hash = "sha256:e3ab5d32784e843fc0dd3ab6dcafc67ef806e6b6828dc6af2f689be0eb4d781d"}, + {file = "numpy-1.24.2-cp38-cp38-win_amd64.whl", hash = "sha256:76807b4063f0002c8532cfeac47a3068a69561e9c8715efdad3c642eb27c0756"}, + {file = "numpy-1.24.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4199e7cfc307a778f72d293372736223e39ec9ac096ff0a2e64853b866a8e18a"}, + {file = "numpy-1.24.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:adbdce121896fd3a17a77ab0b0b5eedf05a9834a18699db6829a64e1dfccca7f"}, + {file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:889b2cc88b837d86eda1b17008ebeb679d82875022200c6e8e4ce6cf549b7acb"}, + {file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f64bb98ac59b3ea3bf74b02f13836eb2e24e48e0ab0145bbda646295769bd780"}, + {file = "numpy-1.24.2-cp39-cp39-win32.whl", hash = "sha256:63e45511ee4d9d976637d11e6c9864eae50e12dc9598f531c035265991910468"}, + {file = "numpy-1.24.2-cp39-cp39-win_amd64.whl", hash = "sha256:a77d3e1163a7770164404607b7ba3967fb49b24782a6ef85d9b5f54126cc39e5"}, + {file = "numpy-1.24.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:92011118955724465fb6853def593cf397b4a1367495e0b59a7e69d40c4eb71d"}, + {file = "numpy-1.24.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9006288bcf4895917d02583cf3411f98631275bc67cce355a7f39f8c14338fa"}, + {file = "numpy-1.24.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:150947adbdfeceec4e5926d956a06865c1c690f2fd902efede4ca6fe2e657c3f"}, + {file = "numpy-1.24.2.tar.gz", hash = "sha256:003a9f530e880cb2cd177cba1af7220b9aa42def9c4afc2a2fc3ee6be7eb2b22"}, +] +opencensus = [ + {file = "opencensus-0.11.1-py2.py3-none-any.whl", hash = "sha256:96a4375e97dfc30a5dffcbae6aba32f58e340178e673d0d2db2905ff2df3df20"}, + {file = "opencensus-0.11.1.tar.gz", hash = "sha256:b52dd6b4013c133a6be9f0ff2e9068c6401d3332b0ecc5faef3dd6c42166f6f3"}, +] +opencensus-context = [ + {file = "opencensus-context-0.1.3.tar.gz", hash = "sha256:a03108c3c10d8c80bb5ddf5c8a1f033161fa61972a9917f9b9b3a18517f0088c"}, + {file = "opencensus_context-0.1.3-py2.py3-none-any.whl", hash = "sha256:073bb0590007af276853009fac7e4bab1d523c3f03baf4cb4511ca38967c6039"}, +] opencensus-ext-azure = [ {file = "opencensus-ext-azure-1.0.7.tar.gz", hash = "sha256:4b08a5da92d935df375a9a38bbf8f6fe70713a7911bea7844c71df527c163d89"}, {file = "opencensus_ext_azure-1.0.7-py2.py3-none-any.whl", hash = "sha256:50d24ac185a422760db0dd07a33f545125642855005ffd9bab84ab57be9b9a21"}, @@ -2365,9 +2710,13 @@ opencensus-ext-django = [ {file = "opencensus-ext-django-0.7.4.tar.gz", hash = "sha256:ab2d22d876c811c5897c90d44cf225efe49c6fd255a7640e3d0063dd9d7f85d3"}, {file = "opencensus_ext_django-0.7.4-py2.py3-none-any.whl", hash = "sha256:e5b8f156ac3705c8e8c2cedf2084c934070bdb81d7d1cf88aca8393b8ab93999"}, ] +openpyxl = [ + {file = "openpyxl-3.0.10-py2.py3-none-any.whl", hash = "sha256:0ab6d25d01799f97a9464630abacbb34aafecdcaa0ef3cba6d6b3499867d0355"}, + {file = "openpyxl-3.0.10.tar.gz", hash = "sha256:e47805627aebcf860edb4edf7987b1309c1b3632f3750538ed962bbcc3bd7449"}, +] packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, ] pandas = [ {file = "pandas-1.3.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:62d5b5ce965bae78f12c1c0df0d387899dd4211ec0bdc52822373f13a3a022b9"}, @@ -2404,7 +2753,7 @@ pdf2image = [ {file = "pdf2image-1.16.0-py3-none-any.whl", hash = "sha256:84f79f2b8fad943e36323ea4e937fcb05f26ded0caa0a01181df66049e42fb65"}, {file = "pdf2image-1.16.0.tar.gz", hash = "sha256:d58ed94d978a70c73c2bb7fdf8acbaf2a7089c29ff8141be5f45433c0c4293bb"}, ] -"pdfminer.six" = [ +pdfminer-six = [ {file = "pdfminer.six-20191110-py2.py3-none-any.whl", hash = "sha256:ca2ca58f3ac66a486bce53a6ddba95dc2b27781612915fa41c444790ba9cd2a8"}, {file = "pdfminer.six-20191110.tar.gz", hash = "sha256:141a53ec491bee6d45bf9b2c7f82601426fb5d32636bcf6b9c8a8f3b6431fea6"}, ] @@ -2417,41 +2766,67 @@ pickleshare = [ {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] pillow = [ - {file = "Pillow-9.0.1-1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5d24e1d674dd9d72c66ad3ea9131322819ff86250b30dc5821cbafcfa0b96b4"}, - {file = "Pillow-9.0.1-1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2632d0f846b7c7600edf53c48f8f9f1e13e62f66a6dbc15191029d950bfed976"}, - {file = "Pillow-9.0.1-1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9618823bd237c0d2575283f2939655f54d51b4527ec3972907a927acbcc5bfc"}, - {file = "Pillow-9.0.1-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:9bfdb82cdfeccec50aad441afc332faf8606dfa5e8efd18a6692b5d6e79f00fd"}, - {file = "Pillow-9.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5100b45a4638e3c00e4d2320d3193bdabb2d75e79793af7c3eb139e4f569f16f"}, - {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:528a2a692c65dd5cafc130de286030af251d2ee0483a5bf50c9348aefe834e8a"}, - {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f29d831e2151e0b7b39981756d201f7108d3d215896212ffe2e992d06bfe049"}, - {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:855c583f268edde09474b081e3ddcd5cf3b20c12f26e0d434e1386cc5d318e7a"}, - {file = "Pillow-9.0.1-cp310-cp310-win32.whl", hash = "sha256:d9d7942b624b04b895cb95af03a23407f17646815495ce4547f0e60e0b06f58e"}, - {file = "Pillow-9.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81c4b81611e3a3cb30e59b0cf05b888c675f97e3adb2c8672c3154047980726b"}, - {file = "Pillow-9.0.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:413ce0bbf9fc6278b2d63309dfeefe452835e1c78398efb431bab0672fe9274e"}, - {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80fe64a6deb6fcfdf7b8386f2cf216d329be6f2781f7d90304351811fb591360"}, - {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cef9c85ccbe9bee00909758936ea841ef12035296c748aaceee535969e27d31b"}, - {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d19397351f73a88904ad1aee421e800fe4bbcd1aeee6435fb62d0a05ccd1030"}, - {file = "Pillow-9.0.1-cp37-cp37m-win32.whl", hash = "sha256:d21237d0cd37acded35154e29aec853e945950321dd2ffd1a7d86fe686814669"}, - {file = "Pillow-9.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ede5af4a2702444a832a800b8eb7f0a7a1c0eed55b644642e049c98d589e5092"}, - {file = "Pillow-9.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:b5b3f092fe345c03bca1e0b687dfbb39364b21ebb8ba90e3fa707374b7915204"}, - {file = "Pillow-9.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:335ace1a22325395c4ea88e00ba3dc89ca029bd66bd5a3c382d53e44f0ccd77e"}, - {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db6d9fac65bd08cea7f3540b899977c6dee9edad959fa4eaf305940d9cbd861c"}, - {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f154d173286a5d1863637a7dcd8c3437bb557520b01bddb0be0258dcb72696b5"}, - {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d4b1341ac07ae07eb2cc682f459bec932a380c3b122f5540432d8977e64eae"}, - {file = "Pillow-9.0.1-cp38-cp38-win32.whl", hash = "sha256:effb7749713d5317478bb3acb3f81d9d7c7f86726d41c1facca068a04cf5bb4c"}, - {file = "Pillow-9.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:7f7609a718b177bf171ac93cea9fd2ddc0e03e84d8fa4e887bdfc39671d46b00"}, - {file = "Pillow-9.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:80ca33961ced9c63358056bd08403ff866512038883e74f3a4bf88ad3eb66838"}, - {file = "Pillow-9.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c3c33ac69cf059bbb9d1a71eeaba76781b450bc307e2291f8a4764d779a6b28"}, - {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12875d118f21cf35604176872447cdb57b07126750a33748bac15e77f90f1f9c"}, - {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:514ceac913076feefbeaf89771fd6febde78b0c4c1b23aaeab082c41c694e81b"}, - {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3c5c79ab7dfce6d88f1ba639b77e77a17ea33a01b07b99840d6ed08031cb2a7"}, - {file = "Pillow-9.0.1-cp39-cp39-win32.whl", hash = "sha256:718856856ba31f14f13ba885ff13874be7fefc53984d2832458f12c38205f7f7"}, - {file = "Pillow-9.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:f25ed6e28ddf50de7e7ea99d7a976d6a9c415f03adcaac9c41ff6ff41b6d86ac"}, - {file = "Pillow-9.0.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97"}, - {file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253e8a302a96df6927310a9d44e6103055e8fb96a6822f8b7f514bb7ef77de56"}, - {file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6295f6763749b89c994fcb6d8a7f7ce03c3992e695f89f00b741b4580b199b7e"}, - {file = "Pillow-9.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a9f44cd7e162ac6191491d7249cceb02b8116b0f7e847ee33f739d7cb1ea1f70"}, - {file = "Pillow-9.0.1.tar.gz", hash = "sha256:6c8bc8238a7dfdaf7a75f5ec5a663f4173f8c367e5a39f87e720495e1eed75fa"}, + {file = "Pillow-9.3.0-1-cp37-cp37m-win32.whl", hash = "sha256:e6ea6b856a74d560d9326c0f5895ef8050126acfdc7ca08ad703eb0081e82b74"}, + {file = "Pillow-9.3.0-1-cp37-cp37m-win_amd64.whl", hash = "sha256:32a44128c4bdca7f31de5be641187367fe2a450ad83b833ef78910397db491aa"}, + {file = "Pillow-9.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:0b7257127d646ff8676ec8a15520013a698d1fdc48bc2a79ba4e53df792526f2"}, + {file = "Pillow-9.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b90f7616ea170e92820775ed47e136208e04c967271c9ef615b6fbd08d9af0e3"}, + {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68943d632f1f9e3dce98908e873b3a090f6cba1cbb1b892a9e8d97c938871fbe"}, + {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be55f8457cd1eac957af0c3f5ece7bc3f033f89b114ef30f710882717670b2a8"}, + {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d77adcd56a42d00cc1be30843d3426aa4e660cab4a61021dc84467123f7a00c"}, + {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:829f97c8e258593b9daa80638aee3789b7df9da5cf1336035016d76f03b8860c"}, + {file = "Pillow-9.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:801ec82e4188e935c7f5e22e006d01611d6b41661bba9fe45b60e7ac1a8f84de"}, + {file = "Pillow-9.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:871b72c3643e516db4ecf20efe735deb27fe30ca17800e661d769faab45a18d7"}, + {file = "Pillow-9.3.0-cp310-cp310-win32.whl", hash = "sha256:655a83b0058ba47c7c52e4e2df5ecf484c1b0b0349805896dd350cbc416bdd91"}, + {file = "Pillow-9.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:9f47eabcd2ded7698106b05c2c338672d16a6f2a485e74481f524e2a23c2794b"}, + {file = "Pillow-9.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:57751894f6618fd4308ed8e0c36c333e2f5469744c34729a27532b3db106ee20"}, + {file = "Pillow-9.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7db8b751ad307d7cf238f02101e8e36a128a6cb199326e867d1398067381bff4"}, + {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3033fbe1feb1b59394615a1cafaee85e49d01b51d54de0cbf6aa8e64182518a1"}, + {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22b012ea2d065fd163ca096f4e37e47cd8b59cf4b0fd47bfca6abb93df70b34c"}, + {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a65733d103311331875c1dca05cb4606997fd33d6acfed695b1232ba1df193"}, + {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:502526a2cbfa431d9fc2a079bdd9061a2397b842bb6bc4239bb176da00993812"}, + {file = "Pillow-9.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:90fb88843d3902fe7c9586d439d1e8c05258f41da473952aa8b328d8b907498c"}, + {file = "Pillow-9.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:89dca0ce00a2b49024df6325925555d406b14aa3efc2f752dbb5940c52c56b11"}, + {file = "Pillow-9.3.0-cp311-cp311-win32.whl", hash = "sha256:3168434d303babf495d4ba58fc22d6604f6e2afb97adc6a423e917dab828939c"}, + {file = "Pillow-9.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:18498994b29e1cf86d505edcb7edbe814d133d2232d256db8c7a8ceb34d18cef"}, + {file = "Pillow-9.3.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:772a91fc0e03eaf922c63badeca75e91baa80fe2f5f87bdaed4280662aad25c9"}, + {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa4107d1b306cdf8953edde0534562607fe8811b6c4d9a486298ad31de733b2"}, + {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4012d06c846dc2b80651b120e2cdd787b013deb39c09f407727ba90015c684f"}, + {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77ec3e7be99629898c9a6d24a09de089fa5356ee408cdffffe62d67bb75fdd72"}, + {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:6c738585d7a9961d8c2821a1eb3dcb978d14e238be3d70f0a706f7fa9316946b"}, + {file = "Pillow-9.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:828989c45c245518065a110434246c44a56a8b2b2f6347d1409c787e6e4651ee"}, + {file = "Pillow-9.3.0-cp37-cp37m-win32.whl", hash = "sha256:82409ffe29d70fd733ff3c1025a602abb3e67405d41b9403b00b01debc4c9a29"}, + {file = "Pillow-9.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:41e0051336807468be450d52b8edd12ac60bebaa97fe10c8b660f116e50b30e4"}, + {file = "Pillow-9.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:b03ae6f1a1878233ac620c98f3459f79fd77c7e3c2b20d460284e1fb370557d4"}, + {file = "Pillow-9.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4390e9ce199fc1951fcfa65795f239a8a4944117b5935a9317fb320e7767b40f"}, + {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40e1ce476a7804b0fb74bcfa80b0a2206ea6a882938eaba917f7a0f004b42502"}, + {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0a06a052c5f37b4ed81c613a455a81f9a3a69429b4fd7bb913c3fa98abefc20"}, + {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03150abd92771742d4a8cd6f2fa6246d847dcd2e332a18d0c15cc75bf6703040"}, + {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:15c42fb9dea42465dfd902fb0ecf584b8848ceb28b41ee2b58f866411be33f07"}, + {file = "Pillow-9.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:51e0e543a33ed92db9f5ef69a0356e0b1a7a6b6a71b80df99f1d181ae5875636"}, + {file = "Pillow-9.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3dd6caf940756101205dffc5367babf288a30043d35f80936f9bfb37f8355b32"}, + {file = "Pillow-9.3.0-cp38-cp38-win32.whl", hash = "sha256:f1ff2ee69f10f13a9596480335f406dd1f70c3650349e2be67ca3139280cade0"}, + {file = "Pillow-9.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:276a5ca930c913f714e372b2591a22c4bd3b81a418c0f6635ba832daec1cbcfc"}, + {file = "Pillow-9.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:73bd195e43f3fadecfc50c682f5055ec32ee2c933243cafbfdec69ab1aa87cad"}, + {file = "Pillow-9.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c7c8ae3864846fc95f4611c78129301e203aaa2af813b703c55d10cc1628535"}, + {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e0918e03aa0c72ea56edbb00d4d664294815aa11291a11504a377ea018330d3"}, + {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0915e734b33a474d76c28e07292f196cdf2a590a0d25bcc06e64e545f2d146c"}, + {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af0372acb5d3598f36ec0914deed2a63f6bcdb7b606da04dc19a88d31bf0c05b"}, + {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ad58d27a5b0262c0c19b47d54c5802db9b34d38bbf886665b626aff83c74bacd"}, + {file = "Pillow-9.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:97aabc5c50312afa5e0a2b07c17d4ac5e865b250986f8afe2b02d772567a380c"}, + {file = "Pillow-9.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9aaa107275d8527e9d6e7670b64aabaaa36e5b6bd71a1015ddd21da0d4e06448"}, + {file = "Pillow-9.3.0-cp39-cp39-win32.whl", hash = "sha256:bac18ab8d2d1e6b4ce25e3424f709aceef668347db8637c2296bcf41acb7cf48"}, + {file = "Pillow-9.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:b472b5ea442148d1c3e2209f20f1e0bb0eb556538690fa70b5e1f79fa0ba8dc2"}, + {file = "Pillow-9.3.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ab388aaa3f6ce52ac1cb8e122c4bd46657c15905904b3120a6248b5b8b0bc228"}, + {file = "Pillow-9.3.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbb8e7f2abee51cef77673be97760abff1674ed32847ce04b4af90f610144c7b"}, + {file = "Pillow-9.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca31dd6014cb8b0b2db1e46081b0ca7d936f856da3b39744aef499db5d84d02"}, + {file = "Pillow-9.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c7025dce65566eb6e89f56c9509d4f628fddcedb131d9465cacd3d8bac337e7e"}, + {file = "Pillow-9.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ebf2029c1f464c59b8bdbe5143c79fa2045a581ac53679733d3a91d400ff9efb"}, + {file = "Pillow-9.3.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b59430236b8e58840a0dfb4099a0e8717ffb779c952426a69ae435ca1f57210c"}, + {file = "Pillow-9.3.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12ce4932caf2ddf3e41d17fc9c02d67126935a44b86df6a206cf0d7161548627"}, + {file = "Pillow-9.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae5331c23ce118c53b172fa64a4c037eb83c9165aba3a7ba9ddd3ec9fa64a699"}, + {file = "Pillow-9.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0b07fffc13f474264c336298d1b4ce01d9c5a011415b79d4ee5527bb69ae6f65"}, + {file = "Pillow-9.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:073adb2ae23431d3b9bcbcff3fe698b62ed47211d0716b067385538a1b0f28b8"}, + {file = "Pillow-9.3.0.tar.gz", hash = "sha256:c935a22a557a560108d780f9a0fc426dd7459940dc54faa49d83249c8d3e760f"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -2464,9 +2839,41 @@ polib = [ promise = [ {file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"}, ] -prompt-toolkit = [] -protobuf = [] -psutil = [] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.37-py3-none-any.whl", hash = "sha256:6a2948ec427dfcc7c983027b1044b355db6aaa8be374f54ad2015471f7d81c5b"}, + {file = "prompt_toolkit-3.0.37.tar.gz", hash = "sha256:d5d73d4b5eb1a92ba884a88962b157f49b71e06c4348b417dd622b25cdd3800b"}, +] +protobuf = [ + {file = "protobuf-4.22.0-cp310-abi3-win32.whl", hash = "sha256:b2fea9dc8e3c0f32c38124790ef16cba2ee0628fe2022a52e435e1117bfef9b1"}, + {file = "protobuf-4.22.0-cp310-abi3-win_amd64.whl", hash = "sha256:a33a273d21852f911b8bda47f39f4383fe7c061eb1814db2c76c9875c89c2491"}, + {file = "protobuf-4.22.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:e894e9ae603e963f0842498c4cd5d39c6a60f0d7e4c103df50ee939564298658"}, + {file = "protobuf-4.22.0-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:7c535d126e7dcc714105ab20b418c4fedbd28f8b8afc42b7350b1e317bbbcc71"}, + {file = "protobuf-4.22.0-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:86c3d20428b007537ba6792b475c0853bba7f66b1f60e610d913b77d94b486e4"}, + {file = "protobuf-4.22.0-cp37-cp37m-win32.whl", hash = "sha256:1669cb7524221a8e2d9008d0842453dbefdd0fcdd64d67672f657244867635fb"}, + {file = "protobuf-4.22.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ab4d043865dd04e6b09386981fe8f80b39a1e46139fb4a3c206229d6b9f36ff6"}, + {file = "protobuf-4.22.0-cp38-cp38-win32.whl", hash = "sha256:29288813aacaa302afa2381db1d6e0482165737b0afdf2811df5fa99185c457b"}, + {file = "protobuf-4.22.0-cp38-cp38-win_amd64.whl", hash = "sha256:e474b63bab0a2ea32a7b26a4d8eec59e33e709321e5e16fb66e766b61b82a95e"}, + {file = "protobuf-4.22.0-cp39-cp39-win32.whl", hash = "sha256:47d31bdf58222dd296976aa1646c68c6ee80b96d22e0a3c336c9174e253fd35e"}, + {file = "protobuf-4.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:c27f371f0159feb70e6ea52ed7e768b3f3a4c5676c1900a7e51a24740381650e"}, + {file = "protobuf-4.22.0-py3-none-any.whl", hash = "sha256:c3325803095fb4c2a48649c321d2fbde59f8fbfcb9bfc7a86df27d112831c571"}, + {file = "protobuf-4.22.0.tar.gz", hash = "sha256:652d8dfece122a24d98eebfef30e31e455d300efa41999d1182e015984ac5930"}, +] +psutil = [ + {file = "psutil-5.9.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8"}, + {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe"}, + {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549"}, + {file = "psutil-5.9.4-cp27-cp27m-win32.whl", hash = "sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad"}, + {file = "psutil-5.9.4-cp27-cp27m-win_amd64.whl", hash = "sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94"}, + {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24"}, + {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7"}, + {file = "psutil-5.9.4-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7"}, + {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1"}, + {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08"}, + {file = "psutil-5.9.4-cp36-abi3-win32.whl", hash = "sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff"}, + {file = "psutil-5.9.4-cp36-abi3-win_amd64.whl", hash = "sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4"}, + {file = "psutil-5.9.4-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e"}, + {file = "psutil-5.9.4.tar.gz", hash = "sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62"}, +] psycopg2 = [ {file = "psycopg2-2.8.6-cp27-cp27m-win32.whl", hash = "sha256:068115e13c70dc5982dfc00c5d70437fe37c014c808acce119b5448361c03725"}, {file = "psycopg2-2.8.6-cp27-cp27m-win_amd64.whl", hash = "sha256:d160744652e81c80627a909a0e808f3c6653a40af435744de037e3172cf277f5"}, @@ -2492,39 +2899,13 @@ pure-eval = [ {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, ] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] pyasn1 = [ - {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, - {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, - {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, - {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, - {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, - {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, - {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, - {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, - {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, - {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, - {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, ] pyasn1-modules = [ {file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"}, - {file = "pyasn1_modules-0.2.8-py2.4.egg", hash = "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199"}, - {file = "pyasn1_modules-0.2.8-py2.5.egg", hash = "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"}, - {file = "pyasn1_modules-0.2.8-py2.6.egg", hash = "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb"}, - {file = "pyasn1_modules-0.2.8-py2.7.egg", hash = "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8"}, {file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"}, - {file = "pyasn1_modules-0.2.8-py3.1.egg", hash = "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d"}, - {file = "pyasn1_modules-0.2.8-py3.2.egg", hash = "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45"}, - {file = "pyasn1_modules-0.2.8-py3.3.egg", hash = "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4"}, - {file = "pyasn1_modules-0.2.8-py3.4.egg", hash = "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811"}, - {file = "pyasn1_modules-0.2.8-py3.5.egg", hash = "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed"}, - {file = "pyasn1_modules-0.2.8-py3.6.egg", hash = "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0"}, - {file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"}, ] pycountry = [ {file = "pycountry-19.8.18.tar.gz", hash = "sha256:3c57aa40adcf293d59bebaffbe60d8c39976fba78d846a018dc0c2ec9c6cb3cb"}, @@ -2533,40 +2914,85 @@ pycparser = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] -pycryptodome = [] +pycryptodome = [ + {file = "pycryptodome-3.17-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:2c5631204ebcc7ae33d11c43037b2dafe25e2ab9c1de6448eb6502ac69c19a56"}, + {file = "pycryptodome-3.17-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:04779cc588ad8f13c80a060b0b1c9d1c203d051d8a43879117fe6b8aaf1cd3fa"}, + {file = "pycryptodome-3.17-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:f812d58c5af06d939b2baccdda614a3ffd80531a26e5faca2c9f8b1770b2b7af"}, + {file = "pycryptodome-3.17-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:9453b4e21e752df8737fdffac619e93c9f0ec55ead9a45df782055eb95ef37d9"}, + {file = "pycryptodome-3.17-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:121d61663267f73692e8bde5ec0d23c9146465a0d75cad75c34f75c752527b01"}, + {file = "pycryptodome-3.17-cp27-cp27m-win32.whl", hash = "sha256:ba2d4fcb844c6ba5df4bbfee9352ad5352c5ae939ac450e06cdceff653280450"}, + {file = "pycryptodome-3.17-cp27-cp27m-win_amd64.whl", hash = "sha256:87e2ca3aa557781447428c4b6c8c937f10ff215202ab40ece5c13a82555c10d6"}, + {file = "pycryptodome-3.17-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f44c0d28716d950135ff21505f2c764498eda9d8806b7c78764165848aa419bc"}, + {file = "pycryptodome-3.17-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5a790bc045003d89d42e3b9cb3cc938c8561a57a88aaa5691512e8540d1ae79c"}, + {file = "pycryptodome-3.17-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:d086d46774e27b280e4cece8ab3d87299cf0d39063f00f1e9290d096adc5662a"}, + {file = "pycryptodome-3.17-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:5587803d5b66dfd99e7caa31ed91fba0fdee3661c5d93684028ad6653fce725f"}, + {file = "pycryptodome-3.17-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:e7debd9c439e7b84f53be3cf4ba8b75b3d0b6e6015212355d6daf44ac672e210"}, + {file = "pycryptodome-3.17-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ca1ceb6303be1282148f04ac21cebeebdb4152590842159877778f9cf1634f09"}, + {file = "pycryptodome-3.17-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:dc22cc00f804485a3c2a7e2010d9f14a705555f67020eb083e833cabd5bd82e4"}, + {file = "pycryptodome-3.17-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80ea8333b6a5f2d9e856ff2293dba2e3e661197f90bf0f4d5a82a0a6bc83a626"}, + {file = "pycryptodome-3.17-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c133f6721fba313722a018392a91e3c69d3706ae723484841752559e71d69dc6"}, + {file = "pycryptodome-3.17-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:333306eaea01fde50a73c4619e25631e56c4c61bd0fb0a2346479e67e3d3a820"}, + {file = "pycryptodome-3.17-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:1a30f51b990994491cec2d7d237924e5b6bd0d445da9337d77de384ad7f254f9"}, + {file = "pycryptodome-3.17-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:909e36a43fe4a8a3163e9c7fc103867825d14a2ecb852a63d3905250b308a4e5"}, + {file = "pycryptodome-3.17-cp35-abi3-win32.whl", hash = "sha256:a3228728a3808bc9f18c1797ec1179a0efb5068c817b2ffcf6bcd012494dffb2"}, + {file = "pycryptodome-3.17-cp35-abi3-win_amd64.whl", hash = "sha256:9ec565e89a6b400eca814f28d78a9ef3f15aea1df74d95b28b7720739b28f37f"}, + {file = "pycryptodome-3.17-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:e1819b67bcf6ca48341e9b03c2e45b1c891fa8eb1a8458482d14c2805c9616f2"}, + {file = "pycryptodome-3.17-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:f8e550caf52472ae9126953415e4fc554ab53049a5691c45b8816895c632e4d7"}, + {file = "pycryptodome-3.17-pp27-pypy_73-win32.whl", hash = "sha256:afbcdb0eda20a0e1d44e3a1ad6d4ec3c959210f4b48cabc0e387a282f4c7deb8"}, + {file = "pycryptodome-3.17-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a74f45aee8c5cc4d533e585e0e596e9f78521e1543a302870a27b0ae2106381e"}, + {file = "pycryptodome-3.17-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38bbd6717eac084408b4094174c0805bdbaba1f57fc250fd0309ae5ec9ed7e09"}, + {file = "pycryptodome-3.17-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f68d6c8ea2974a571cacb7014dbaada21063a0375318d88ac1f9300bc81e93c3"}, + {file = "pycryptodome-3.17-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8198f2b04c39d817b206ebe0db25a6653bb5f463c2319d6f6d9a80d012ac1e37"}, + {file = "pycryptodome-3.17-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3a232474cd89d3f51e4295abe248a8b95d0332d153bf46444e415409070aae1e"}, + {file = "pycryptodome-3.17-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4992ec965606054e8326e83db1c8654f0549cdb26fce1898dc1a20bc7684ec1c"}, + {file = "pycryptodome-3.17-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53068e33c74f3b93a8158dacaa5d0f82d254a81b1002e0cd342be89fcb3433eb"}, + {file = "pycryptodome-3.17-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:74794a2e2896cd0cf56fdc9db61ef755fa812b4a4900fa46c49045663a92b8d0"}, + {file = "pycryptodome-3.17.tar.gz", hash = "sha256:bce2e2d8e82fcf972005652371a3e8731956a0c1fbb719cc897943b3695ad91b"}, +] pydash = [ {file = "pydash-4.8.0-py2.py3-none-any.whl", hash = "sha256:611ad40e3b11fb57396cca4a55ea04641200a1dd632c3c7e2c14849bee386625"}, {file = "pydash-4.8.0.tar.gz", hash = "sha256:546afa043ed1defa3122383bebe8b7072f43554ccc5f0c4360638f99e5ed7327"}, ] -pygments = [] -pyparsing = [] +pygments = [ + {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, + {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, +] pypdf2 = [ {file = "PyPDF2-1.26.0.tar.gz", hash = "sha256:e28f902f2f0a1603ea95ebe21dff311ef09be3d0f0ef29a3e44a932729564385"}, ] pyrsistent = [ - {file = "pyrsistent-0.18.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1"}, - {file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d45866ececf4a5fff8742c25722da6d4c9e180daa7b405dc0a2a2790d668c26"}, - {file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ed6784ceac462a7d6fcb7e9b663e93b9a6fb373b7f43594f9ff68875788e01e"}, - {file = "pyrsistent-0.18.1-cp310-cp310-win32.whl", hash = "sha256:e4f3149fd5eb9b285d6bfb54d2e5173f6a116fe19172686797c056672689daf6"}, - {file = "pyrsistent-0.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:636ce2dc235046ccd3d8c56a7ad54e99d5c1cd0ef07d9ae847306c91d11b5fec"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e92a52c166426efbe0d1ec1332ee9119b6d32fc1f0bbfd55d5c1088070e7fc1b"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7a096646eab884bf8bed965bad63ea327e0d0c38989fc83c5ea7b8a87037bfc"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdfd2c361b8a8e5d9499b9082b501c452ade8bbf42aef97ea04854f4a3f43b22"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-win32.whl", hash = "sha256:7ec335fc998faa4febe75cc5268a9eac0478b3f681602c1f27befaf2a1abe1d8"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6455fc599df93d1f60e1c5c4fe471499f08d190d57eca040c0ea182301321286"}, - {file = "pyrsistent-0.18.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fd8da6d0124efa2f67d86fa70c851022f87c98e205f0594e1fae044e7119a5a6"}, - {file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bfe2388663fd18bd8ce7db2c91c7400bf3e1a9e8bd7d63bf7e77d39051b85ec"}, - {file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e3e1fcc45199df76053026a51cc59ab2ea3fc7c094c6627e93b7b44cdae2c8c"}, - {file = "pyrsistent-0.18.1-cp38-cp38-win32.whl", hash = "sha256:b568f35ad53a7b07ed9b1b2bae09eb15cdd671a5ba5d2c66caee40dbf91c68ca"}, - {file = "pyrsistent-0.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1b96547410f76078eaf66d282ddca2e4baae8964364abb4f4dcdde855cd123a"}, - {file = "pyrsistent-0.18.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f87cc2863ef33c709e237d4b5f4502a62a00fab450c9e020892e8e2ede5847f5"}, - {file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc66318fb7ee012071b2792024564973ecc80e9522842eb4e17743604b5e045"}, - {file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:914474c9f1d93080338ace89cb2acee74f4f666fb0424896fcfb8d86058bf17c"}, - {file = "pyrsistent-0.18.1-cp39-cp39-win32.whl", hash = "sha256:1b34eedd6812bf4d33814fca1b66005805d3640ce53140ab8bbb1e2651b0d9bc"}, - {file = "pyrsistent-0.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:e24a828f57e0c337c8d8bb9f6b12f09dfdf0273da25fda9e314f0b684b415a07"}, - {file = "pyrsistent-0.18.1.tar.gz", hash = "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96"}, -] -pytest = [] + {file = "pyrsistent-0.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a"}, + {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64"}, + {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf"}, + {file = "pyrsistent-0.19.3-cp310-cp310-win32.whl", hash = "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a"}, + {file = "pyrsistent-0.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da"}, + {file = "pyrsistent-0.19.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9"}, + {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393"}, + {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19"}, + {file = "pyrsistent-0.19.3-cp311-cp311-win32.whl", hash = "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3"}, + {file = "pyrsistent-0.19.3-cp311-cp311-win_amd64.whl", hash = "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-win32.whl", hash = "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b"}, + {file = "pyrsistent-0.19.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8"}, + {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a"}, + {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c"}, + {file = "pyrsistent-0.19.3-cp38-cp38-win32.whl", hash = "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c"}, + {file = "pyrsistent-0.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7"}, + {file = "pyrsistent-0.19.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc"}, + {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2"}, + {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3"}, + {file = "pyrsistent-0.19.3-cp39-cp39-win32.whl", hash = "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2"}, + {file = "pyrsistent-0.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98"}, + {file = "pyrsistent-0.19.3-py3-none-any.whl", hash = "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64"}, + {file = "pyrsistent-0.19.3.tar.gz", hash = "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440"}, +] +pytest = [ + {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, + {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, +] pytest-django = [ {file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"}, {file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"}, @@ -2578,9 +3004,7 @@ pytest-ordering = [ ] pytest-profiling = [ {file = "pytest-profiling-1.7.0.tar.gz", hash = "sha256:93938f147662225d2b8bd5af89587b979652426a8a6ffd7e73ec4a23e24b7f29"}, - {file = "pytest_profiling-1.7.0-py2.7.egg", hash = "sha256:3b255f9db36cb2dd7536a8e7e294c612c0be7f7850a7d30754878e4315d56600"}, {file = "pytest_profiling-1.7.0-py2.py3-none-any.whl", hash = "sha256:999cc9ac94f2e528e3f5d43465da277429984a1c237ae9818f8cfd0b06acb019"}, - {file = "pytest_profiling-1.7.0-py3.6.egg", hash = "sha256:6bce4e2edc04409d2f3158c16750fab8074f62d404cc38eeb075dff7fcbb996c"}, ] python-bidi = [ {file = "python-bidi-0.4.2.tar.gz", hash = "sha256:5347f71e82b3e9976dc657f09ded2bfe39ba8d6777ca81a5b2c56c30121c496e"}, @@ -2604,7 +3028,10 @@ pytz = [ {file = "pytz-2019.1-py2.py3-none-any.whl", hash = "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda"}, {file = "pytz-2019.1.tar.gz", hash = "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141"}, ] -redis = [] +redis = [ + {file = "redis-4.5.1-py3-none-any.whl", hash = "sha256:5deb072d26e67d2be1712603bfb7947ec3431fb0eec9c578994052e33035af6d"}, + {file = "redis-4.5.1.tar.gz", hash = "sha256:1eec3741cda408d3a5f84b78d089c8b8d895f21b3b050988351e925faf202864"}, +] reportlab = [ {file = "reportlab-3.6.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a09acda69357664190a02f239abb01505d519a2563ba89d57d6fb55ca14ade72"}, {file = "reportlab-3.6.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a681047247a6d896ed7ec18b95054c9c139c0269417beb066985244b8d18f75"}, @@ -2643,41 +3070,76 @@ reportlab = [ {file = "reportlab-3.6.6-cp39-cp39-win_amd64.whl", hash = "sha256:e80ed55cbbaf905635a2673d439495e1b1925b8379ea56aa2fc859a00e41af9f"}, {file = "reportlab-3.6.6.tar.gz", hash = "sha256:dd1cdb62dc123f5859ca514eb639f70660bdc818c95fb0ee2370a175a0e20ce4"}, ] -requests = [] -requests-toolbelt = [] -rsa = [] -rx = [] +requests = [ + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, +] +requests-toolbelt = [ + {file = "requests-toolbelt-0.10.1.tar.gz", hash = "sha256:62e09f7ff5ccbda92772a29f394a49c3ad6cb181d568b1337626b2abb628a63d"}, + {file = "requests_toolbelt-0.10.1-py2.py3-none-any.whl", hash = "sha256:18565aa58116d9951ac39baa288d3adb5b3ff975c4f25eee78555d89e8f247f7"}, +] +rsa = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] +rx = [ + {file = "Rx-1.6.3.tar.gz", hash = "sha256:ca71b65d0fc0603a3b5cfaa9e33f5ba81e4aae10a58491133595088d7734b2da"}, +] s3transfer = [ {file = "s3transfer-0.5.2-py3-none-any.whl", hash = "sha256:7a6f4c4d1fdb9a2b640244008e142cbc2cd3ae34b386584ef044dd0f27101971"}, {file = "s3transfer-0.5.2.tar.gz", hash = "sha256:95c58c194ce657a5f4fb0b9e60a84968c808888aed628cd98ab8771fe1db98ed"}, ] +sentry-sdk = [ + {file = "sentry-sdk-1.15.0.tar.gz", hash = "sha256:69ecbb2e1ff4db02a06c4f20f6f69cb5dfe3ebfbc06d023e40d77cf78e9c37e7"}, + {file = "sentry_sdk-1.15.0-py2.py3-none-any.whl", hash = "sha256:7ad4d37dd093f4a7cb5ad804c6efe9e8fab8873f7ffc06042dc3f3fd700a93ec"}, +] +setuptools = [ + {file = "setuptools-67.4.0-py3-none-any.whl", hash = "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"}, + {file = "setuptools-67.4.0.tar.gz", hash = "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330"}, +] singledispatch = [ - {file = "singledispatch-3.7.0-py2.py3-none-any.whl", hash = "sha256:bc77afa97c8a22596d6d4fc20f1b7bdd2b86edc2a65a4262bdd7cc3cc19aa989"}, - {file = "singledispatch-3.7.0.tar.gz", hash = "sha256:c1a4d5c1da310c3fd8fccfb8d4e1cb7df076148fd5d858a819e37fffe44f3092"}, + {file = "singledispatch-4.0.0-py2.py3-none-any.whl", hash = "sha256:b8f69397a454b45b91e2f949fcc87896c53718ca59aab6367966e8b3f010ec77"}, + {file = "singledispatch-4.0.0.tar.gz", hash = "sha256:f3c327a968651a7f4b03586eab7d90a07b05ff3ef7942d1967036eb9f75ab8fc"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] snapshottest = [ - {file = "snapshottest-0.5.1.tar.gz", hash = "sha256:2cc7157e77674ea8ebeb2351466ff50cd4b5ad8e213adc06df9c16a75ab5bafc"}, + {file = "snapshottest-0.6.0-py2.py3-none-any.whl", hash = "sha256:9b177cffe0870c589df8ddbee0a770149c5474b251955bdbde58b7f32a4ec429"}, + {file = "snapshottest-0.6.0.tar.gz", hash = "sha256:bbcaf81d92d8e330042e5c928e13d9f035e99e91b314fe55fda949c2f17b653c"}, ] sortedcontainers = [ {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] -sqlparse = [] -stack-data = [] +sqlparse = [ + {file = "sqlparse-0.4.3-py3-none-any.whl", hash = "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34"}, + {file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"}, +] +stack-data = [ + {file = "stack_data-0.6.2-py3-none-any.whl", hash = "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8"}, + {file = "stack_data-0.6.2.tar.gz", hash = "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815"}, +] tabula-py = [ {file = "tabula_py-1.2.0-py2.py3-none-any.whl", hash = "sha256:f87932ca3afb4a62fe6ebcddadc9f51151b1a8c375e151a17389974c0182ec37"}, ] -termcolor = [] -text-unidecode = [] +termcolor = [ + {file = "termcolor-2.2.0-py3-none-any.whl", hash = "sha256:91ddd848e7251200eac969846cbae2dacd7d71c2871e92733289e7e3666f48e7"}, + {file = "termcolor-2.2.0.tar.gz", hash = "sha256:dfc8ac3f350788f23b2947b3e6cfa5a53b630b612e6cd8965a015a776020b99a"}, +] +text-unidecode = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -traitlets = [] +traitlets = [ + {file = "traitlets-5.9.0-py3-none-any.whl", hash = "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8"}, + {file = "traitlets-5.9.0.tar.gz", hash = "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9"}, +] typing = [ {file = "typing-3.6.2-py2-none-any.whl", hash = "sha256:349b1f9c109c84b53ac79ac1d822eaa68fc91d63b321bd9392df15098f746f53"}, {file = "typing-3.6.2-py3-none-any.whl", hash = "sha256:63a8255fe7c6269916baa440eb9b6a67139b0b97a01af632e7bd2842e1e02f15"}, @@ -2690,7 +3152,10 @@ uritemplate = [ {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, ] -urllib3 = [] +urllib3 = [ + {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, + {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, +] vine = [ {file = "vine-5.0.0-py2.py3-none-any.whl", hash = "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30"}, {file = "vine-5.0.0.tar.gz", hash = "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"}, @@ -2728,14 +3193,13 @@ wasmer-compiler-cranelift = [ {file = "wasmer_compiler_cranelift-1.1.0-py3-none-any.whl", hash = "sha256:200fea80609cfb088457327acf66d5aa61f4c4f66b5a71133ada960b534c7355"}, ] wcwidth = [ - {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, - {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, + {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, + {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, ] webencodings = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] -wrapt = [] xhtml2pdf = [ {file = "xhtml2pdf-0.2.5.tar.gz", hash = "sha256:6797e974fac66f0efbe927c1539a2756ca4fe8777eaa5882bac132fc76b39421"}, ] diff --git a/pyproject.toml b/pyproject.toml index cb1c5e1bf..536707245 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ python = "^3.8" # Better than a wired-in subversion: Django = ">=3.2,<3.3" Markdown = "==3.3.4" -Pillow = "==9.0.1" +Pillow = "==9.3.0" PyPDF2 = "==1.26.0" pdf2image = "==1.16.0" azure-storage = "==0.36.0" @@ -26,7 +26,7 @@ choicesenum = "==0.7.0" coreapi = "==2.3.3" coreschema = "==0.0.4" coverage = "==4.4.2" -cryptography = "==38.0.1" +cryptography = "==39.0.1" django-admin-autocomplete-filter = "==0.5" django-admin-list-filter-dropdown = "==1.0.2" django-cors-headers = "==3.11.0" @@ -47,7 +47,7 @@ djangorestframework = "==3.11.2" djangorestframework-camel-case = "==1.2.0" django-environ = "==0.8.1" # upgrade elasticsearch? Keep synched with init-es.sh: a new docker image (es vm) is also needed. Then manage.py index_elasticsearch!" -elasticsearch = "==6.8.2" +elasticsearch = "==7.0.0" factory_boy = "==2.12.0" fuzzywuzzy = "==0.17.0" graphene-django = "^2.15.0" @@ -68,6 +68,7 @@ python-Levenshtein = "==0.12.1" python-dateutil = "==2.8.0" python-mimeparse = "==1.6.0" pytz = "==2019.1" +openpyxl = "==3.0.10" requests = "==2.28.1" tabula-py = "==1.2.0" typing = "==3.6.2" @@ -77,6 +78,8 @@ xhtml2pdf = "==0.2.5" reportlab = "==3.6.6" # XXX: Used by xhtml2pdf reportlab==3.6.7 breaks for now celery = { version = "==5.1.2", extras = ["redis"] } django-redis = "==5.0.0" +sentry-sdk = "*" +django-haystack = { version = "*", extras = ["elasticsearch"] } mapbox-tilesets = "*" ipython = "*" @@ -85,8 +88,8 @@ ipython = "*" pytest-profiling = "*" pytest-ordering = "*" pytest-django = "*" -snapshottest = "==0.5.1" -django-debug-toolbar = "==2.2.1" +snapshottest = "==0.6.0" +django-debug-toolbar = "==3.8.1" [tool.pyright] extraPaths = ["apps"]
    DREF Request @@ -361,7 +361,7 @@ {{ fr.other_fields.dref_amount|floatformat:"1"|intcomma|slice:":-2" }}
    Imminent DREF @@ -379,7 +379,7 @@ {{ fr.other_fields.imminent_dref_amount|floatformat:"1"|intcomma|slice:":-2" }}
    ERU Base Camp @@ -397,7 +397,7 @@ {{ fr.other_fields.eru_base_camp_units|floatformat:"1"|intcomma|slice:":-2" }}
    ERU basic health care @@ -415,7 +415,7 @@ {{ fr.other_fields.eru_basic_health_care_units|floatformat:"1"|intcomma|slice:":-2" }}
    ERU deployment Hospital @@ -433,7 +433,7 @@ {{ fr.other_fields.eru_deployment_hospital_units|floatformat:"1"|intcomma|slice:":-2" }}
    ERU IT Telecom @@ -451,7 +451,7 @@ {{ fr.other_fields.eru_it_telecom_units|floatformat:"1"|intcomma|slice:":-2" }}
    ERU Logistics @@ -469,7 +469,7 @@ {{ fr.other_fields.eru_logistics_units|floatformat:"1"|intcomma|slice:":-2" }}
    ERU referral Hospital @@ -487,7 +487,7 @@ {{ fr.other_fields.eru_referral_hospital_units|floatformat:"1"|intcomma|slice:":-2" }}
    ERU Relief @@ -505,7 +505,7 @@ {{ fr.other_fields.eru_relief_units|floatformat:"1"|intcomma|slice:":-2" }}
    ERU water sanitation 15: @@ -523,7 +523,7 @@ {{ fr.other_fields.eru_water_sanitation_15_units|floatformat:"1"|intcomma|slice:":-2" }}
    ERU water sanitation 20 @@ -541,7 +541,7 @@ {{ fr.other_fields.eru_water_sanitation_20_units|floatformat:"1"|intcomma|slice:":-2" }}
    ERU water sanitation 40 @@ -559,7 +559,7 @@ {{ fr.other_fields.eru_water_sanitation_40_units|floatformat:"1"|intcomma|slice:":-2" }}
    Forecast Based Action @@ -732,13 +732,13 @@
    Emergency Appeal - {{ fr.other_fields.appeal.name }}