nordabiz/scripts/migrate_rada_data.py
Maciej Pienczyn 554adc2aa0
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
fix(migration): Fix email_verified in secretary creation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 15:28:22 +01:00

294 lines
20 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Migrate Rada Izby data to production.
Creates/updates board members and creates meetings.
Safe: checks for existing users by email before creating.
"""
import os
import sys
from datetime import datetime, date, time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from database import SessionLocal, User, BoardMeeting, SystemRole
from werkzeug.security import generate_password_hash
db = SessionLocal()
# --- Step 1: Create/update Rada members ---
RADA_MEMBERS = [
{"name": "Andrzej Gorczycki", "email": "a.gorczycki@ekofabrykawejherowo.pl"},
{"name": "Angelika Piechocka", "email": "info@greenhousesystems.pl"},
{"name": "Artur Wiertel", "email": "artur.wiertel@waterm.pl"},
{"name": "Aureliusz Jędrzejewski", "email": "a.jedrzejewski@scrol.pl"},
{"name": "Dariusz Schmidtke", "email": "dariusz.schmidtke@tkchopin.pl"},
{"name": "Iwona Musiał", "email": "iwonamusial@cristap.pl"},
{"name": "Jacek Pomieczyński", "email": "jacek.pomieczynski@eura-tech.eu"},
{"name": "Jakub Bornowski", "email": "kuba@bormax.com.pl"},
{"name": "Janusz Masiak", "email": "jm@hebel-masiak.pl"},
{"name": "Krzysztof Kubis", "email": "krzysztof.kubis@sibuk.pl"},
{"name": "Leszek Glaza", "email": "leszek@rotor.pl"},
{"name": "Michał Węsierski", "email": "mjwesierski@gmail.com"},
{"name": "Paweł Kwidziński", "email": "pawel.kwidzinski@norda-biznes.info"},
{"name": "Paweł Piechota", "email": "pawel.piechota@norda-biznes.info"},
{"name": "Radosław Skwarło", "email": "radoslaw@skwarlo.pl"},
{"name": "Roman Wierciński", "email": "roman@sigmabudownictwo.pl"},
]
# Map: email -> prod user id (for attendance remapping)
email_to_prod_id = {}
for member in RADA_MEMBERS:
user = db.query(User).filter(User.email == member["email"]).first()
if user:
# Existing user - just set rada flag
if not user.is_rada_member:
user.is_rada_member = True
print(f" UPDATED: {user.name} ({user.email}) -> is_rada_member=True")
else:
print(f" OK: {user.name} ({user.email}) already rada member")
email_to_prod_id[member["email"]] = user.id
else:
# Create new user with random password (they'll need to reset)
import secrets
temp_password = secrets.token_urlsafe(16)
new_user = User(
email=member["email"],
name=member["name"],
password_hash=generate_password_hash(temp_password),
is_active=True,
is_rada_member=True,
role=SystemRole.MEMBER.name,
is_verified=True,
)
db.add(new_user)
db.flush() # Get ID
email_to_prod_id[member["email"]] = new_user.id
print(f" CREATED: {new_user.name} ({new_user.email}) ID={new_user.id}")
# Secretary (not a rada member, but needed for meeting reference)
secretary_email = "magdalena.kloska@norda-biznes.info"
sec_user = db.query(User).filter(User.email == secretary_email).first()
if not sec_user:
import secrets
temp_password = secrets.token_urlsafe(16)
sec_user = User(
email=secretary_email,
name="Magdalena Klóska",
password_hash=generate_password_hash(temp_password),
is_active=True,
is_rada_member=False,
role=SystemRole.MEMBER.name,
is_verified=True,
)
db.add(sec_user)
db.flush()
print(f" CREATED (secretary): {sec_user.name} ({sec_user.email}) ID={sec_user.id}")
else:
print(f" OK (secretary): {sec_user.name} ({sec_user.email})")
# Deactivate corrupted duplicate account (ID=40)
corrupted = db.query(User).filter(User.email == "a.gorczycki@ekofabrykawejherowo.plherowo.pl").first()
if corrupted and corrupted.is_active:
corrupted.is_active = False
print(f" DEACTIVATED: ID={corrupted.id} ({corrupted.email}) - corrupted duplicate")
db.commit()
print(f"\nRada members: {len(email_to_prod_id)} processed")
# --- Step 2: Staging user ID -> Prod user ID mapping ---
# Staging attendance uses staging user IDs. We need to remap.
# Staging ID -> email mapping (from staging export)
STAGING_ID_TO_EMAIL = {
"13": "jm@hebel-masiak.pl",
"14": "jacek.pomieczynski@eura-tech.eu",
"18": "info@greenhousesystems.pl",
"35": "leszek@rotor.pl",
"36": "iwonamusial@cristap.pl",
"37": "a.gorczycki@ekofabrykawejherowo.pl",
"38": "pawel.kwidzinski@norda-biznes.info",
"39": "dariusz.schmidtke@tkchopin.pl",
"40": "artur.wiertel@waterm.pl",
"41": "a.jedrzejewski@scrol.pl",
"42": "krzysztof.kubis@sibuk.pl",
"43": "kuba@bormax.com.pl",
"44": "pawel.piechota@norda-biznes.info",
"45": "radoslaw@skwarlo.pl",
"46": "roman@sigmabudownictwo.pl",
"47": "mjwesierski@gmail.com",
}
def remap_attendance(staging_attendance):
"""Remap staging user IDs to production user IDs in attendance dict"""
if not staging_attendance:
return None
remapped = {}
for staging_id, data in staging_attendance.items():
email = STAGING_ID_TO_EMAIL.get(staging_id)
if email and email in email_to_prod_id:
prod_id = str(email_to_prod_id[email])
remapped[prod_id] = data
else:
print(f" WARNING: Could not remap staging ID {staging_id}")
return remapped
# --- Step 3: Create meetings ---
# Get chairperson and secretary IDs on production
chairperson = db.query(User).filter(User.email == "leszek@rotor.pl").first()
secretary = db.query(User).filter(User.email == "magdalena.kloska@norda-biznes.info").first()
# Get admin user as fallback for created_by
admin = db.query(User).filter(User.role == 'ADMIN', User.is_active == True).first()
created_by_id = admin.id if admin else 1
chairperson_id = chairperson.id if chairperson else None
secretary_id = secretary.id if secretary else None
# Check if meetings already exist
existing = db.query(BoardMeeting).filter(BoardMeeting.year == 2026).all()
if existing:
print(f"\nWARNING: {len(existing)} meetings already exist for 2026. Skipping creation.")
else:
# Meeting 1/2026 (Jan 7)
m1_attendance = {
"13": {"present": False, "initials": "JM"},
"14": {"present": True, "initials": "JP"},
"18": {"present": True, "initials": "AP"},
"35": {"present": True, "initials": "LG"},
"36": {"present": True, "initials": "IM"},
"37": {"present": False, "initials": "AG"},
"38": {"present": True, "initials": "PK"},
"39": {"present": False, "initials": "DS"},
"40": {"present": True, "initials": "AW"},
"41": {"present": False, "initials": "AJ"},
"42": {"present": False, "initials": "KK"},
"43": {"present": True, "initials": "JB"},
"44": {"present": False, "initials": "PP"},
"45": {"present": True, "initials": "RS"},
"46": {"present": False, "initials": "RW"},
"47": {"present": False, "initials": "MW"},
}
m1_proceedings = [
{"tasks": [], "title": "Akceptacja programu posiedzenia", "decisions": ["Program posiedzenia przyjęty."], "discussion": "Przyjęto program posiedzenia.", "agenda_item": 0},
{"tasks": [], "title": "Zbieranie kworum", "decisions": ["Kworum potwierdzone."], "discussion": "Stwierdzono kworum: 8 z 16 członków obecnych.", "agenda_item": 1},
{"tasks": ["AW przekazanie członkom Rady materiałów: kalendarz wydarzeń + prezentacja strategii", "Członkowie Rady przesłanie uwag do strategii marki i komunikacji termin: do końca stycznia na adres Artur.wiertel@waterm.pl", "LG koordynacja przygotowań do spotkania mentorskiego Akademii Biznesu NORDA (mentor: Wojciech Gębarowski, Orlex)", "IM wstępna propozycja terminu i formuły kuligu (warianty + orientacyjne koszty)", "IM sprawdzenie dostępności organizatora / miejsca kuligu i warunków realizacji", "AW przygotowanie i przedstawienie na spotkaniu czwartkowym: (1) kalendarz 2026, (2) skrót strategii komunikacji, (3) projekt CRM termin: 29/01/2026", "AW / MP (INPI) doprecyzowanie funkcji CRM (forum i panel ogłoszeń) oraz propozycja etapów wdrożenia termin do ustalenia z Radą", "Członkowie Rady przygotowanie krótkiej propozycji/założeń dot. aktywizacji nowych członków i podniesienia prestiżu Rady (na potrzeby przygotowań do Walnego) termin: luty", "LG i AW wstępne rozeznanie organizacyjno-kosztowe dla: (1) Chorwacja jachty, (2) misja Chiny termin: luty", "RW przypomnienie zasad korzystania z grupy WhatsApp (komunikaty informacyjne, minimum treści pobocznych)"], "title": "Planowanie wydarzeń dla Izby na rok 2026", "decisions": ["Członkowie Rady zgłoszą uwagi i propozycje do przedstawionej strategii marki i komunikacji.", "Spotkanie mentorskie Akademii Biznesu NORDA jest przygotowywane; temat kontynuowany organizacyjnie.", "Wątek Walnego Zgromadzenia wyborczego (czerwiec) wymaga przygotowania koncepcji aktywizowania nowych członków oraz działań wzmacniających prestiż Rady do dalszego omówienia na kolejnym posiedzeniu.", "Projekt CRM (INPI) przyjęto do dalszych prac; zebrane zostaną uwagi funkcjonalne, w tym dotyczące forum i panelu ogłoszeń.", "Na czwartkowym spotkaniu networkingowym Chwila dla Biznesu członkom Izby zostaną przedstawione: kalendarz 2026, skrót strategii komunikacji oraz projekt CRM.", "Temat kuligu przyjęto do szybkiego rozpoznania pod kątem realności terminu i warunków organizacyjnych.", "Mniejsze wydarzenia integracyjne mogą mieć uproszczoną organizację, jednak komunikacja o nich ma docierać do wszystkich członków Izby.", "Propozycje wyjazdów integracyjnych (Chorwacja / Chiny) przyjęto do dalszego rozpoznania (zainteresowanie, koszty, terminy, organizacja).", "Grupa WhatsApp (RW) pozostaje kanałem krótkich komunikatów dla wszystkich, z ograniczeniem liczby wiadomości."], "discussion": "AW przedstawił propozycję kalendarza wydarzeń Izby na 2026 rok wraz ze wstępnym omówieniem planowanych aktywności. Podkreślono, że główny nacisk w 2026 roku powinien zostać położony na rozpoczęcie przygotowań do jubileuszu 30-lecia Izby NORDA.\n\nAW zaprezentował założenia strategii Izby NORDA w obszarze promocji marki i komunikacji, przygotowanej przez firmę Your Welcome (Agnieszka Kulinkowska) przy udziale przedstawicieli Zarządu: AW oraz PKw. Ustalono, że prezentacja ma charakter roboczy i służy doprecyzowaniu strategii, a członkowie Rady mają wnieść uwagi i propozycje.\n\nLG poinformował o prowadzonych przygotowaniach do spotkania mentorskiego w ramach Akademii Biznesu NORDA; w roli mentora ma wystąpić Pan Wojciech Gębarowski, właściciel firmy Orlex.\n\nOmówiono przygotowania do czerwcowego Walnego Zgromadzenia wyborczego, na którym zostanie powołana nowa Rada. W dyskusji pojawił się pomysł aktywizowania nowych członków oraz podniesienia prestiżu Rady.\n\nAW przedstawił projekt platformy CRM przygotowywanej przez członka Izby Macieja Pieńczyna (firma INPI). Platforma ma służyć komunikacji pomiędzy administracją a członkami oraz pomiędzy samymi członkami. Ustalono kierunek rozbudowy CRM jako pogłębionej platformy wymiany informacji (m.in. forum oraz panel ogłoszeń). Platforma ma stanowić zamkniętą część portalu www.nordabiznes.pl, dostępną wyłącznie dla członków Izby.\n\nIM zaproponowała inicjatywę integracyjną w formie kuligu, możliwą do realizacji jeszcze w styczniu (w zależności od warunków pogodowych). Uznano, że mniejsze wydarzenia integracyjne mogą mieć uproszczoną organizację, ale informacja o możliwości udziału powinna trafiać do wszystkich członków Izby.\n\nAW zaproponował, aby w ramach czwartkowych spotkań networkingowych przygotować prezentację dla członków Izby obejmującą: (1) kalendarz wydarzeń na 2026 rok, (2) skrót strategii komunikacji, (3) prezentację projektu CRM.\n\nPrzedstawiono propozycje wyjazdów integracyjnych: rejs jachtowy w Chorwacji (wrzesień 2026) oraz misja gospodarcza do Chin (październiklistopad 2026).\n\nUznano, że grupa WhatsApp prowadzona przez RW ma pełnić funkcję krótkiego komunikatora do wszystkich, z minimalną liczbą wiadomości niezwiązanych bezpośrednio z informacją dla członków.", "agenda_item": 2},
{"tasks": [], "title": "Prezentacja: 303 Łukasz Mielewczyk (kandydat na członka Izby)", "decisions": ["Prezentacja przeniesiona na kolejny miesiąc."], "discussion": "Kandydat nie stawił się, wcześniej poinformował o nagłej sytuacji.", "agenda_item": 3},
{"tasks": ["MK przedstawić na SM i stronie www nowego członka, możliwie jak najszybciej", "Otoczyć opieką nowego członka przez członka Izby (nie koniecznie z Rady), zaprosić na spotkanie 29/01/2026 Hotel Olimp"], "title": "Prezentacja: Iwona Speniak coach/mentoring (kandydatka na członka Izby)", "decisions": ["Przyjęta jednogłośnie przez Radę."], "discussion": "Obszar działania: aktywizacja kobiet, warsztaty psychoedukacyjne.", "agenda_item": 4},
{"tasks": [], "title": "Głosowanie nad kandydaturami", "decisions": [], "discussion": "Głosowanie przeprowadzono w ramach poszczególnych prezentacji.", "agenda_item": 5},
{"tasks": [], "title": "Propozycja stworzenia ogólnodostępnej listy członków do kontaktów", "decisions": ["Temat włączony do projektu CRM."], "discussion": "Omówiono w ramach przygotowań do CRM.", "agenda_item": 6},
{"tasks": [], "title": "Budżet i planowanie wydatków na rok 2026 oraz planowanie wysokości składek 2026", "decisions": ["Temat przeniesiony na kolejne posiedzenie."], "discussion": "Punkt przeniesiony na kolejną Radę.", "agenda_item": 7},
{"tasks": [], "title": "Wolne wnioski", "decisions": ["Termin kolejnego spotkania: 04/02/2026."], "discussion": "Zaproponowano przenieść godzinę kolejnej Rady na 16:00.", "agenda_item": 8},
{"tasks": [], "title": "Ustalenie daty kolejnego posiedzenia", "decisions": ["Kolejne posiedzenie Rady: 04.02.2026 o godz. 16:00."], "discussion": "Ustalono datę 04/02/2026, godzina 16:00.", "agenda_item": 9},
{"tasks": [], "title": "Zakończenie posiedzenia", "decisions": [], "discussion": "Prowadzący zamknął posiedzenie o godz. 18:30.", "agenda_item": 10},
]
meeting1 = BoardMeeting(
meeting_number=1,
year=2026,
meeting_date=date(2026, 1, 7),
start_time=time(16, 0),
end_time=time(18, 30),
location="Siedziba Izby",
chairperson_id=chairperson_id,
secretary_id=secretary_id,
guests="brak",
agenda_items=[
{"title": "Akceptacja programu posiedzenia", "number": 1, "time_end": "16:10", "time_start": "16:00"},
{"title": "Zbieranie kworum", "number": 2, "time_end": "16:15", "time_start": "16:10"},
{"title": "Planowanie wydarzen dla Izby na rok 2026", "number": 3, "time_end": "16:40", "time_start": "16:15"},
{"title": "Prezentacja: 303 - Lukasz Mielewczyk (kandydat)", "number": 4, "time_end": "16:50", "time_start": "16:40"},
{"title": "Prezentacja: Iwona Speniak - coach/mentoring", "number": 5, "time_end": "17:15", "time_start": "17:00"},
{"title": "Glosowanie nad kandydaturami", "number": 6, "time_end": "17:20", "time_start": "17:15"},
{"title": "Propozycja listy czlonkow do kontaktow", "number": 7, "time_end": "17:30", "time_start": "17:20"},
{"title": "Budzet i planowanie wydatkow na rok 2026", "number": 8, "time_end": "17:50", "time_start": "17:30"},
{"title": "Wolne wnioski", "number": 9, "time_end": "18:00", "time_start": "17:50"},
{"title": "Ustalenie daty kolejnego posiedzenia", "number": 10},
{"title": "Zakonczenie posiedzenia", "number": 11},
],
attendance=remap_attendance(m1_attendance),
quorum_count=8,
quorum_confirmed=True,
proceedings=m1_proceedings,
status="protocol_published",
created_by=created_by_id,
agenda_published_at=datetime(2026, 1, 6, 12, 0),
protocol_published_at=datetime(2026, 1, 15, 12, 0),
)
db.add(meeting1)
# Meeting 2/2026 (Feb 4)
m2_attendance = {
"13": {"present": None, "initials": "JM"},
"14": {"present": None, "initials": "JP"},
"18": {"present": None, "initials": "AP"},
"35": {"present": None, "initials": "LG"},
"36": {"present": None, "initials": "IM"},
"37": {"present": None, "initials": "AG"},
"38": {"present": None, "initials": "PK"},
"39": {"present": None, "initials": "DS"},
"40": {"present": None, "initials": "AW"},
"41": {"present": None, "initials": "AJ"},
"42": {"present": None, "initials": "KK"},
"43": {"present": None, "initials": "JB"},
"44": {"present": None, "initials": "PP"},
"45": {"present": None, "initials": "RS"},
"46": {"present": None, "initials": "RW"},
"47": {"present": None, "initials": "MW"},
}
meeting2 = BoardMeeting(
meeting_number=2,
year=2026,
meeting_date=date(2026, 2, 4),
start_time=time(16, 0),
end_time=time(19, 0),
location="Siedziba Izby",
chairperson_id=chairperson_id,
secretary_id=secretary_id,
guests=None,
agenda_items=[
{"title": "Akceptacja programu posiedzenia", "number": 1, "time_end": "16:10", "time_start": "16:00"},
{"title": "Zbieranie kworum", "number": 2, "time_end": "16:15", "time_start": "16:10"},
{"title": "Prezentacja firmy Konkol - kandydat na czlonka Izby", "number": 3, "time_end": "16:40", "time_start": "16:30"},
{"title": "Prezentacja firmy Ibet - kandydat na czlonka Izby", "number": 4, "time_end": "17:00", "time_start": "16:45"},
{"title": "Prezentacja firmy Audioline Sp. z o.o. - kandydat na czlonka Izby", "number": 5, "time_end": "17:15", "time_start": "17:00"},
{"title": "Prezentacja firmy Pc Invest - kandydat na czlonka Izby", "number": 6, "time_end": "17:30", "time_start": "17:15"},
{"title": "Prezentacja firmy Pacific Sun - kandydat na czlonka Izby", "number": 7, "time_end": "17:40", "time_start": "17:30"},
{"title": "Glosowanie nad kandydaturami", "number": 8, "time_end": "17:50", "time_start": "17:40"},
{"title": "Omowienie strategii i dzialan marketingowych", "number": 9, "time_end": "18:05", "time_start": "17:50"},
{"title": "Wysokosc skladek na rok 2026 - plany", "number": 10, "time_end": "18:20", "time_start": "18:05"},
{"title": "Planowane wydarzenia integracyjne - grill wiosenny", "number": 11, "time_end": "18:30", "time_start": "18:20"},
{"title": "Wybor komitetu organizacyjnego na 30-lecie Izby", "number": 12, "time_end": "18:45", "time_start": "18:30"},
{"title": "Sprawozdanie z realizacji zadan ze stycznia oraz wolne wnioski", "number": 13, "time_end": "19:00", "time_start": "18:45"},
{"title": "Ustalenie daty kolejnego posiedzenia (04.03)", "number": 14},
{"title": "Zakonczenie posiedzenia", "number": 15},
],
attendance=remap_attendance(m2_attendance),
quorum_count=None,
quorum_confirmed=None,
proceedings=None,
status="agenda_published",
created_by=created_by_id,
agenda_published_at=datetime(2026, 2, 3, 12, 0),
protocol_published_at=None,
)
db.add(meeting2)
db.commit()
print("\nMeetings created: 2")
# --- Verify ---
meetings = db.query(BoardMeeting).filter(BoardMeeting.year == 2026).all()
members = db.query(User).filter(User.is_rada_member == True, User.is_active == True).all()
print(f"\n=== VERIFICATION ===")
print(f"Meetings 2026: {len(meetings)}")
for m in meetings:
print(f" {m.meeting_identifier} | {m.meeting_date} | status={m.status}")
print(f"Rada members: {len(members)}")
for u in sorted(members, key=lambda x: x.name or ''):
print(f" {u.name} ({u.email})")
db.close()
print("\nDone!")