diff --git a/district/forms.py b/district/forms.py index 45b12efafb86376aa240cb2a98fcf530fa4e5405..af137ed79c83a6f933d7fcb069bea07a7c264f4b 100644 --- a/district/forms.py +++ b/district/forms.py @@ -1,13 +1,15 @@ import os from shared.forms import JekyllImportForm as SharedJekyllImportForm +from shared.forms import OctopusPeopleImportForm as SharedOctopusPeopleImportForm from .tasks import import_jekyll_articles +from .tasks import import_people_from_group class JekyllImportForm(SharedJekyllImportForm): def handle_import(self): - lock_file_name = f"/tmp/.{self.instance.id}.import-lock" + lock_file_name = f"/tmp/.{self.instance.id}.articles-import-lock" if os.path.isfile(lock_file_name): return @@ -21,3 +23,21 @@ class JekyllImportForm(SharedJekyllImportForm): dry_run=self.cleaned_data["dry_run"], use_git=True, ) + + +class OctopusImportForm(SharedOctopusPeopleImportForm): + def handle_import_from_group(self): + lock_file_name = f"/tmp/.{self.instance.id}.people-from-group-import-lock" + + if os.path.isfile(lock_file_name): + return + + open(lock_file_name, "w").close() + + import_people_from_group.delay( + people_parent_page_id=self.instance.id, + collection_id=self.cleaned_data["collection"].id, + group_shortcut=self.cleaned_data["group_shortcut"], + dry_run=self.cleaned_data["dry_run"], + lock_file_name=lock_file_name + ) \ No newline at end of file diff --git a/district/models.py b/district/models.py index 4ee9414e8280393102c5ddd5477f05fb13cb53d1..10fad543451eb066838e281e0540655e491e1584 100644 --- a/district/models.py +++ b/district/models.py @@ -69,7 +69,7 @@ from shared.utils import ( ) from . import blocks -from .forms import JekyllImportForm +from .forms import JekyllImportForm, OctopusImportForm CONTENT_BLOCKS = DEFAULT_CONTENT_BLOCKS + [ ("chart", ChartBlock()), @@ -390,6 +390,8 @@ class DistrictPersonPage(MainPersonPageMixin): class DistrictPeoplePage(MainPeoplePageMixin): + base_form_class = OctopusImportForm + content = StreamField( [ ("people_group", blocks.PeopleGroupBlock(label="Seznam osob")), @@ -403,6 +405,26 @@ class DistrictPeoplePage(MainPeoplePageMixin): parent_page_types = ["district.DistrictHomePage"] subpage_types = ["district.DistrictPersonPage"] + import_panels = [ + MultiFieldPanel( + [ + FieldPanel("do_import"), + FieldPanel("collection"), + FieldPanel("dry_run"), + FieldPanel("group_shortcut") + ], + "import osob z Chobotnice", + ), + ] + + edit_handler = TabbedInterface( + [ + ObjectList(MainPeoplePageMixin.content_panels, heading="Obsah"), + ObjectList(MainPeoplePageMixin.promote_panels, heading="Metadata"), + ObjectList(import_panels, heading="Import"), + ] + ) + class DistrictCalendarPage(SubpageMixin, MetadataPageMixin, CalendarMixin, Page): ### PANELS diff --git a/district/tasks.py b/district/tasks.py index 4870b7020008da8079a433f2307b1d35cbae169a..0e6413255b53df6ff50b7199db93dd98977ae374 100644 --- a/district/tasks.py +++ b/district/tasks.py @@ -3,6 +3,7 @@ import logging from celery import shared_task from shared.jekyll_import import JekyllArticleImporter +from shared.people_import import PeopleGroupImporter logger = logging.getLogger(__name__) @@ -26,3 +27,24 @@ def import_jekyll_articles( use_git=use_git, page_model=DistrictArticlePage, ).perform_import() + + +@shared_task() +def import_people_from_group( + people_parent_page_id, + collection_id, + group_shortcut, + dry_run, + lock_file_name +): + from .models import DistrictPeoplePage, DistrictPersonPage + + return PeopleGroupImporter( + people_parent_page_id=people_parent_page_id, + people_parent_page_model=DistrictPeoplePage, + person_page_model=DistrictPersonPage, + collection_id=collection_id, + group_shortcut=group_shortcut, + dry_run=dry_run, + lock_file_name=lock_file_name, + ).perform_import() \ No newline at end of file diff --git a/majak/settings/base.py b/majak/settings/base.py index a01e7398a7b448dcdd7a76e119b6a6e40e899781..0c69ff0c4fb16ba6ea7bce04b13e78818e9a5626 100644 --- a/majak/settings/base.py +++ b/majak/settings/base.py @@ -269,6 +269,7 @@ WAGTAILADMIN_BASE_URL = BASE_URL # CUSTOM SETTINGS # ------------------------------------------------------------------------------ STYLEGUIDE_URL = env.str("STYLEGUIDE_URL", "https://styleguide.pirati.cz/2.20.x/") +OCTOPUS_API_URL = env.str("OCTOPUS_API_URL", "https://chobotnice.pirati.cz/graphql/") MAJAK_ENV = env.str("MAJAK_ENV", default="prod") diff --git a/requirements/base.in b/requirements/base.in index a9fe3d4b8abc669786c1769ce80d327157570295..ec7fe4126a499ac2f918f7f8f2d01db842e70ccd 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -8,6 +8,7 @@ django-redis django-settings-export django-widget-tweaks django-simple-captcha +gql[all] psycopg2-binary pirates whitenoise==5.3.0 diff --git a/requirements/base.txt b/requirements/base.txt index 99e6ccd5b262e1b57aa9df9215e72a9268c74dc3..05903242b15815365d44fee95ff2cd379f003c75 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,10 +4,18 @@ # # pip-compile base.in # +aiohttp==3.9.5 + # via gql +aiosignal==1.3.1 + # via aiohttp amqp==5.2.0 # via kombu anyascii==0.3.2 # via wagtail +anyio==4.4.0 + # via + # gql + # httpx arrow==1.3.0 # via # -r base.in @@ -18,9 +26,12 @@ asttokens==2.4.1 # via stack-data attrs==23.2.0 # via + # aiohttp # cattrs # ics # requests-cache +backoff==2.2.1 + # via gql beautifulsoup4==4.12.3 # via # -r base.in @@ -29,14 +40,18 @@ billiard==4.2.0 # via celery bleach==6.1.0 # via -r base.in +botocore==1.34.149 + # via gql brotli==1.1.0 # via fonttools cattrs==23.2.3 # via requests-cache celery==5.4.0 # via -r base.in -certifi==2024.6.2 +certifi==2024.7.4 # via + # httpcore + # httpx # requests # sentry-sdk cffi==1.16.0 @@ -57,7 +72,7 @@ click-plugins==1.1.1 # via celery click-repl==0.3.0 # via celery -cryptography==42.0.8 +cryptography==43.0.0 # via # josepy # mozilla-django-oidc @@ -68,7 +83,7 @@ decorator==5.1.1 # via ipython defusedxml==0.7.1 # via willow -django==5.0.6 +django==5.0.7 # via # -r base.in # django-extensions @@ -109,7 +124,7 @@ django-treebeard==4.7.1 # via wagtail django-widget-tweaks==1.5.0 # via -r base.in -djangorestframework==3.15.1 +djangorestframework==3.15.2 # via wagtail draftjs-exporter==5.0.0 # via wagtail @@ -117,26 +132,46 @@ et-xmlfile==1.1.0 # via openpyxl executing==2.0.1 # via stack-data -fastjsonschema==2.19.1 +fastjsonschema==2.20.0 # via -r base.in filetype==1.2.0 # via willow -fonttools[woff]==4.53.0 +fonttools[woff]==4.53.1 # via weasyprint +frozenlist==1.4.1 + # via + # aiohttp + # aiosignal +gql[all]==3.5.0 + # via -r base.in +graphql-core==3.2.3 + # via gql +h11==0.14.0 + # via httpcore html5lib==1.1 # via weasyprint +httpcore==1.0.5 + # via httpx httplib2==0.22.0 # via -r base.in -icalendar==5.0.12 +httpx==0.27.0 + # via gql +icalendar==5.0.13 # via -r base.in ics==0.7.2 # via -r base.in idna==3.7 - # via requests -ipython==8.25.0 + # via + # anyio + # httpx + # requests + # yarl +ipython==8.26.0 # via -r base.in jedi==0.19.1 # via ipython +jmespath==1.0.1 + # via botocore josepy==1.14.0 # via mozilla-django-oidc kombu==5.3.7 @@ -151,35 +186,39 @@ matplotlib-inline==0.1.7 # via ipython mozilla-django-oidc==3.0.0 # via pirates -nh3==0.2.17 +multidict==6.0.5 + # via + # aiohttp + # yarl +nh3==0.2.18 # via -r base.in -numpy==1.26.4 +numpy==2.0.1 # via opencv-python oauthlib==3.2.2 # via # requests-oauthlib # tweepy -opencv-python==4.10.0.82 +opencv-python==4.10.0.84 # via -r base.in -openpyxl==3.1.3 +openpyxl==3.1.5 # via wagtail parso==0.8.4 # via jedi pexpect==4.9.0 # via ipython -pillow==10.3.0 +pillow==10.4.0 # via # django-simple-captcha # pillow-heif # wagtail # weasyprint -pillow-heif==0.16.0 +pillow-heif==0.18.0 # via willow pirates==0.7.0 # via -r base.in platformdirs==4.2.2 # via requests-cache -prompt-toolkit==3.0.46 +prompt-toolkit==3.0.47 # via # click-repl # ipython @@ -187,15 +226,15 @@ psycopg2-binary==2.9.9 # via -r base.in ptyprocess==0.7.0 # via pexpect -pure-eval==0.2.2 +pure-eval==0.2.3 # via stack-data pycparser==2.22 # via cffi -pydyf==0.10.0 +pydyf==0.11.0 # via weasyprint pygments==2.18.0 # via ipython -pyopenssl==24.1.0 +pyopenssl==24.2.1 # via josepy pyparsing==3.1.2 # via httplib2 @@ -206,6 +245,7 @@ pyphen==0.15.0 python-dateutil==2.9.0.post0 # via # arrow + # botocore # celery # icalendar # ics @@ -217,21 +257,25 @@ pytz==2024.1 # l18n pyyaml==6.0.1 # via -r base.in -redis==5.0.5 +redis==5.0.7 # via django-redis requests==2.32.3 # via # -r base.in + # gql # mozilla-django-oidc # requests-cache # requests-oauthlib + # requests-toolbelt # tweepy # wagtail -requests-cache==1.2.0 +requests-cache==1.2.1 # via -r base.in requests-oauthlib==1.3.1 # via tweepy -sentry-sdk==2.5.0 +requests-toolbelt==1.0.0 + # via gql +sentry-sdk==2.11.0 # via -r base.in six==1.16.0 # via @@ -242,9 +286,13 @@ six==1.16.0 # l18n # python-dateutil # url-normalize +sniffio==1.3.1 + # via + # anyio + # httpx soupsieve==2.5 # via beautifulsoup4 -sqlparse==0.5.0 +sqlparse==0.5.1 # via django stack-data==0.6.3 # via ipython @@ -264,14 +312,15 @@ tweepy==4.14.0 # via -r base.in types-python-dateutil==2.9.0.20240316 # via arrow -typing-extensions==4.12.1 +typing-extensions==4.12.2 # via ipython tzdata==2024.1 # via celery url-normalize==1.4.3 # via requests-cache -urllib3==2.2.1 +urllib3==2.2.2 # via + # botocore # requests # requests-cache # sentry-sdk @@ -280,7 +329,7 @@ vine==5.1.0 # amqp # celery # kombu -wagtail==6.1.2 +wagtail==6.1.3 # via # -r base.in # wagtail-metadata @@ -296,7 +345,7 @@ wand==0.6.13 # via -r base.in wcwidth==0.2.13 # via prompt-toolkit -weasyprint==62.2 +weasyprint==62.3 # via -r base.in webencodings==0.5.1 # via @@ -304,11 +353,17 @@ webencodings==0.5.1 # cssselect2 # html5lib # tinycss2 +websockets==11.0.3 + # via gql whitenoise==5.3.0 # via -r base.in willow[heif]==1.8.0 # via # wagtail # willow +yarl==1.9.4 + # via + # aiohttp + # gql zopfli==0.2.3 # via fonttools diff --git a/requirements/dev.txt b/requirements/dev.txt index 5e31598753b52465d30f19eb4f0b1ea8b6413a59..be2a914dccdc42495b33637ebff2bdcd2db2605b 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -6,17 +6,17 @@ # asgiref==3.8.1 # via django -coverage[toml]==7.5.3 +coverage[toml]==7.6.0 # via pytest-cov -django==5.0.6 +django==5.0.7 # via # -r dev.in # django-debug-toolbar -django-debug-toolbar==4.4.2 +django-debug-toolbar==4.4.6 # via -r dev.in factory-boy==3.3.0 # via pytest-factoryboy -faker==25.6.0 +faker==26.0.0 # via factory-boy fastdiff==0.3.0 # via snapshottest @@ -26,14 +26,14 @@ inflection==0.5.1 # via pytest-factoryboy iniconfig==2.0.0 # via pytest -packaging==24.0 +packaging==24.1 # via # pytest # pytest-factoryboy # pytest-sugar pluggy==1.5.0 # via pytest -pytest==8.2.2 +pytest==8.3.2 # via # -r dev.in # pytest-cov @@ -64,7 +64,7 @@ six==1.16.0 # snapshottest snapshottest==0.6.0 # via -r dev.in -sqlparse==0.5.0 +sqlparse==0.5.1 # via # django # django-debug-toolbar @@ -72,7 +72,7 @@ termcolor==2.4.0 # via # pytest-sugar # snapshottest -typing-extensions==4.12.1 +typing-extensions==4.12.2 # via pytest-factoryboy wasmer==1.1.0 # via fastdiff diff --git a/requirements/production.txt b/requirements/production.txt index dd49e7bd479eee29364f50a22416e3665fbd3d6b..ff1dc31cb646d5c98932f06edfd7a74901f0aedc 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -6,5 +6,5 @@ # gunicorn==22.0.0 # via -r production.in -packaging==24.0 +packaging==24.1 # via gunicorn diff --git a/shared/forms.py b/shared/forms.py index b9e90c8fd88acbdb3705287259401c0a14781354..e937d0aa25371702337f7c68a478362e1b6c04ec 100644 --- a/shared/forms.py +++ b/shared/forms.py @@ -9,6 +9,49 @@ class SubscribeForm(forms.Form): return_page_id = forms.IntegerField() +class OctopusPeopleImportForm(WagtailAdminPageForm): + do_import = forms.BooleanField( + initial=False, required=False, label="Provést import osob z Chobotnice" + ) + collection = forms.ModelChoiceField( + queryset=Collection.objects.all(), required=False, label="Kolekce obrázků" + ) + dry_run = forms.BooleanField( + initial=True, + required=False, + label="Jenom na zkoušku", + ) + group_shortcut = forms.CharField( + label="Zkratka skupiny osob", + required=False, + ) + + def clean(self): + cleaned_data = super().clean() + + if not cleaned_data.get("do_import"): + return cleaned_data + + if cleaned_data.get("do_import") and not self.instance.id: + self.add_error( + "do_import", "Import proveďte prosím až po vytvoření stránky" + ) + + if not cleaned_data.get("collection"): + self.add_error("collection", "Pro import je toto pole povinné") + + if not cleaned_data.get("group_shortcut"): + self.add_error("group_shortcut", "Pro import je toto pole povinné") + + return cleaned_data + + def save(self, commit=True): + if self.cleaned_data.get("do_import"): + self.handle_import_from_group() + + return super().save(commit=commit) + + class JekyllImportForm(WagtailAdminPageForm): do_import = forms.BooleanField( initial=False, required=False, label="Provést import z Jekyllu" diff --git a/shared/models/main.py b/shared/models/main.py index 0f2cd59455ec6d0ba61cfdcaa198d56822ce873f..d96f092976aa69d9061f0db8d48e6d6becbedfb2 100644 --- a/shared/models/main.py +++ b/shared/models/main.py @@ -1841,8 +1841,6 @@ class MainPeoplePageMixin( promote_panels = make_promote_panels() - settings_panels = [] - ### RELATIONS # NOTE: Must be overridden diff --git a/shared/people_import.py b/shared/people_import.py new file mode 100644 index 0000000000000000000000000000000000000000..1d72905e8e7e44cb713fcc6274da51cb550a4064 --- /dev/null +++ b/shared/people_import.py @@ -0,0 +1,137 @@ +from gql import gql, Client +from gql.transport.aiohttp import AIOHTTPTransport +from django.conf import settings +import os + + +class PeopleGroupImporter: + def __init__( + self, + people_parent_page_id, + people_parent_page_model, + person_page_model, + collection_id, + group_shortcut, + lock_file_name, + dry_run, + ): + self.people_parent_page_id = people_parent_page_id + self.people_parent_page_model = people_parent_page_model + self.person_page_model = person_page_model + self.collection_id = collection_id + self.group_shortcut = group_shortcut + self.lock_file_name = lock_file_name + self.dry_run = dry_run + + self.transport = AIOHTTPTransport(url=settings.OCTOPUS_API_URL) + self.client = Client(transport=self.transport, fetch_schema_from_transport=True) + + def get_people_ids_from_group(self): + query = gql( + f""" + query {{ + allGroups( + filters: + {{ + shortcut: {{exact: "{self.group_shortcut}" }} + }} + ) {{ + edges {{ + node {{ + memberships {{ + person {{ + id + }} + }} + }} + }} + }} + }} + """ + ) + + result = self.client.execute(query) + + user_ids = [] + + for node in result["allGroups"]["edges"]: + for membership in node["node"]["memberships"]: + user_ids.append(membership["person"]["id"]) + + return user_ids + + def get_person_profile_from_id(self, id: str, kind: str): + query = gql( + f""" + query {{ + allProfiles( + filters: {{ + person: {{ + id: "{id}" + }}, + kind: {kind} + }} + ) {{ + edges {{ + node {{ + email + facebookUrl + flickrUrl + instagramUrl + kind + mastodonUrl + phone + photo + textLong + textShort + tiktokUrl + twitterUrl + url + webUrl + youtubeUrl + person {{ + degreeAfterName + degreeBeforeName + displayName + profilePhoto + }} + }} + }} + }} + }} + """ + ) + + result = self.client.execute(query) + + # Just return the first result, there should never be more than one in this case. + for node in result["allProfiles"]["edges"]: + return node["node"] + + # If there are no results, return None. + return None + + + def perform_import(self): + people_ids = self.get_people_ids_from_group() + + people_profiles = {} + + for person_id in people_ids: + prirotizied_profiles = [] + + prirotizied_profiles.append(self.get_person_profile_from_id(person_id, "POLITICAL")) + prirotizied_profiles.append(self.get_person_profile_from_id(person_id, "PIRATE")) + + for profile in prirotizied_profiles: + if profile is None: + continue + + people_profiles[person_id] = profile + + if person_id not in people_profiles: + people_profiles[person_id] = None + + print(people_profiles) + + os.remove(self.lock_file_name) \ No newline at end of file