diff --git a/kompassi/events/kotaeexpo2026/__init__.py b/kompassi/events/kotaeexpo2026/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kompassi/events/kotaeexpo2026/forms.py b/kompassi/events/kotaeexpo2026/forms.py new file mode 100644 index 000000000..c7c078271 --- /dev/null +++ b/kompassi/events/kotaeexpo2026/forms.py @@ -0,0 +1,242 @@ +from crispy_forms.layout import Fieldset, Layout +from django import forms +from django.db.models import Q + +from kompassi.core.utils import horizontal_form_helper, indented_without_label +from kompassi.labour.forms import AlternativeFormMixin, SignupForm +from kompassi.labour.models import JobCategory, Signup + +from .models import SignupExtra + + +class SignupExtraForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = horizontal_form_helper() + self.helper.form_tag = False + self.helper.layout = Layout( + "shift_type", + "total_work", + indented_without_label("night_shift"), + indented_without_label("overseer"), + "work_days", + Fieldset( + "Työtodistus", + indented_without_label("want_certificate"), + ), + Fieldset( + "Millä kielellä olet valmis palvelemaan asiakkaita?", + "known_language", + "known_language_other", + ), + Fieldset( + "Lisätiedot", + "special_diet", + "special_diet_other", + "accommodation", + "prior_experience", + "shift_wishes", + "free_text", + ), + ) + + class Meta: + model = SignupExtra + fields = ( + "shift_type", + "total_work", + "night_shift", + "overseer", + "work_days", + "want_certificate", + "known_language", + "known_language_other", + "special_diet", + "special_diet_other", + "accommodation", + "prior_experience", + "shift_wishes", + "free_text", + ) + + widgets = dict( + known_language=forms.CheckboxSelectMultiple, + special_diet=forms.CheckboxSelectMultiple, + accommodation=forms.CheckboxSelectMultiple, + work_days=forms.CheckboxSelectMultiple, + ) + + +class OrganizerSignupForm(forms.ModelForm, AlternativeFormMixin): + def __init__(self, *args, **kwargs): + kwargs.pop("event") + admin = kwargs.pop("admin") + + if admin: + raise AssertionError("must not be admin") + + super().__init__(*args, **kwargs) + + self.helper = horizontal_form_helper() + self.helper.form_tag = False + self.helper.layout = Layout( + Fieldset( + "Tehtävän tiedot", + "job_title", + ), + ) + + self.fields["job_title"].help_text = "Mikä on vastuualueesi? Printataan badgeen." + self.fields["job_title"].required = True + + class Meta: + model = Signup + fields = ("job_title",) + + widgets = dict( + job_categories=forms.CheckboxSelectMultiple, + ) + + def get_excluded_m2m_field_defaults(self): + return dict(job_categories=JobCategory.objects.filter(event__slug="kotaeexpo2026", name="Vastaava")) + + def get_excluded_field_defaults(self): + return dict( + total_work="yli10h", + ) + + +class OrganizerSignupExtraForm(forms.ModelForm, AlternativeFormMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = horizontal_form_helper() + self.helper.form_tag = False + self.helper.layout = Layout( + Fieldset( + "Lisätiedot", + "special_diet", + "special_diet_other", + "accommodation", + "email_alias", + ), + ) + + class Meta: + model = SignupExtra + fields = ( + "special_diet", + "special_diet_other", + "accommodation", + "email_alias", + ) + + widgets = dict( + special_diet=forms.CheckboxSelectMultiple, + accommodation=forms.CheckboxSelectMultiple, + ) + + def get_excluded_field_defaults(self): + return dict( + shift_type="kaikkikay", + total_work="yli10h", + night_shift=False, + overseer=False, + want_certificate=False, + prior_experience="", + free_text="Syötetty käyttäen coniitin ilmoittautumislomaketta", + ) + + +class SpecialistSignupForm(SignupForm, AlternativeFormMixin): + def get_job_categories_query(self, event, admin=False): + if admin: + raise AssertionError("must not be admin") + + return Q(event__slug="kotaeexpo2026", public=False) & ~Q(slug="vastaava") + + def get_excluded_field_defaults(self): + return dict( + notes="Syötetty käyttäen erikoistehtävien ilmoittautumislomaketta", + ) + + +class SpecialistSignupExtraForm(forms.ModelForm, AlternativeFormMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = horizontal_form_helper() + self.helper.form_tag = False + self.helper.layout = Layout( + "shift_type", + "total_work", + indented_without_label("night_shift"), + indented_without_label("overseer"), + "work_days", + Fieldset( + "Työtodistus", + indented_without_label("want_certificate"), + ), + Fieldset( + "Millä kielellä olet valmis palvelemaan asiakkaita?", + "known_language", + "known_language_other", + ), + Fieldset( + "Lisätiedot", + "special_diet", + "special_diet_other", + "accommodation", + "prior_experience", + "shift_wishes", + "free_text", + ), + ) + + class Meta: + model = SignupExtra + fields = ( + "shift_type", + "total_work", + "night_shift", + "overseer", + "work_days", + "want_certificate", + "known_language", + "known_language_other", + "special_diet", + "special_diet_other", + "accommodation", + "prior_experience", + "shift_wishes", + "free_text", + ) + + widgets = dict( + known_language=forms.CheckboxSelectMultiple, + special_diet=forms.CheckboxSelectMultiple, + accommodation=forms.CheckboxSelectMultiple, + work_days=forms.CheckboxSelectMultiple, + ) + + +class ShiftWishesSurvey(forms.ModelForm): + def __init__(self, *args, **kwargs): + kwargs.pop("event") + + super().__init__(*args, **kwargs) + + self.helper = horizontal_form_helper() + self.helper.form_tag = False + + @classmethod + def get_instance_for_event_and_person(cls, event, person): + return SignupExtra.objects.get(event=event, person=person) + + class Meta: + model = SignupExtra + fields = ( + "shift_wishes", + "accommodation", + ) + widgets = dict( + accommodation=forms.CheckboxSelectMultiple, + ) diff --git a/kompassi/events/kotaeexpo2026/forms/expense-claim-dimensions.yml b/kompassi/events/kotaeexpo2026/forms/expense-claim-dimensions.yml new file mode 100644 index 000000000..920620056 --- /dev/null +++ b/kompassi/events/kotaeexpo2026/forms/expense-claim-dimensions.yml @@ -0,0 +1,118 @@ +- slug: account + title: + fi: Tiliöinti + en: Account + is_key_dimension: false + choices: + - slug: rent + title: + fi: 4100 Tilavuokrat + en: 4100 Venue rent + - slug: equipment-rental + title: + fi: 4200 Kalustevuokraus + en: 4200 Equipment rental + - slug: restaurants + title: + fi: 4300 Ravintolakulut + en: 4300 Restaurant expenses + - slug: performers + title: + fi: 5100 Esiintyjät + en: 5100 Performers + - slug: program + title: + fi: 5200 Ohjelma + en: 5200 Program + - slug: supplies + title: + fi: 5400 Tarvikkeet + en: 5400 Supplies + - slug: technology + title: + fi: 5500 Tekniikka + en: 5500 Technology + - slug: meeting-expenses + title: + fi: 6100 Kokouskulut + en: 6100 Meeting expenses + - slug: travel-and-accommodation-expenses + title: + fi: 6200 Matka- ja majoituskulut + en: 6200 Travel and accommodation expenses + - slug: advertising-and-marketing-expenses + title: + fi: 8100 Ilmoitus ja markkinointikulut + en: 8100 Advertising and marketing expenses + - slug: representation-expenses + title: + fi: 8200 Edustuskulut + en: 8200 Representation expenses + - slug: multi-year-acquisitions + title: + fi: 9100 Monivuotiset hankinnat + en: 9100 Multi-year acquisitions + - slug: other-event-expenses + title: + fi: 9200 Tapahtuman muut kulut + en: 9200 Other event expenses + - slug: financial-transaction-expenses + title: + fi: 9500 Rahaliikenteen kulut + en: 9500 Financial transaction expenses + - slug: loans-from-individuals + title: + fi: 9550 Lainat yksityishenkilöiltä + en: 9550 Loans from individuals + - slug: loans-from-associations + title: + fi: 9560 Lainat yhdistyksiltä + en: 9560 Loans from associations + - slug: loans-from-banks + title: + fi: 9570 Lainat pankilta + en: 9570 Loans from banks + - slug: insurances + title: + fi: 9700 Vakuutukset + en: 9700 Insurances + - slug: phone-connections + title: + fi: 9800 Puhelinliittymät + en: 9800 Phone connections + - slug: other-operating-expenses + title: + fi: 9900 Muut toiminnan menot + en: 9900 Other operating expenses + +- slug: status + title: + fi: Maksun tila + en: Payment status + is_key_dimension: true + choices: + - slug: new + color: blue + title: + fi: Uusi + en: New + - slug: accepted + color: green + title: + fi: Hyväksytty + en: Accepted + - slug: paid + color: teal + title: + fi: Maksettu + en: Paid + - slug: info-requested + color: yellow + title: + fi: Lisätietoja pyydetty + en: Further details requested + - slug: rejected + color: red + title: + fi: Hylätty + en: Rejected diff --git a/kompassi/events/kotaeexpo2026/forms/expense-claim-en.yml b/kompassi/events/kotaeexpo2026/forms/expense-claim-en.yml new file mode 100644 index 000000000..fa8c96b95 --- /dev/null +++ b/kompassi/events/kotaeexpo2026/forms/expense-claim-en.yml @@ -0,0 +1,59 @@ +title: Expense Claim +description: | + With this form, you can apply for expense reimbursement from Kotae ry for an + event or association-related expense. Fill out the form carefully and attach + all requested attachments. + + If you have not asked for prior approval for the expenses or if you have any + questions about expense reimbursements, please contact us by email at + talous at kotae dot fi or on Discord at @Nimu or + @Aketzu. + +fields: + - slug: title + type: SingleLineText + title: Title + required: true + helpText: Briefly tell us what expense you are applying for reimbursement for or what the invoice is for. + + - slug: description + type: MultiLineText + title: Description + helpText: | + If the title does not tell everything essential, you can provide additional information here. + + - slug: amount + type: DecimalField + minValue: 0 + decimalPlaces: 2 + title: Amount + required: true + helpText: | + How much are you applying for reimbursement or what is the invoice amount? Write the amount in euros. + + - slug: recipient + type: SingleLineText + title: Recipient + required: true + helpText: | + Who will the reimbursement be paid to? Write the first and last name or the company name here. + + - slug: recipient_iban + type: SingleLineText + title: Bank account number of the recipient + required: true + helpText: | + Which account will the reimbursement be paid to? Write the IBAN account number in the format FI12 3456 7890 1234 56. + + - slug: attachments + type: FileUpload + title: Receipts + required: true + multiple: true + helpText: | + Attach all receipts necessary for the expense reimbursement. + The date of the expense, the amount, the VAT rate, and the recipient of the payment must be apparent from the receipts. + For payments made in a currency other than euros, the exchange rate or + the amount in the original currency must also be apparent. + If you do not have a receipt, write a free-form explanation of the expense and + why a receipt is not available, and sign it. diff --git a/kompassi/events/kotaeexpo2026/forms/expense-claim-fi.yml b/kompassi/events/kotaeexpo2026/forms/expense-claim-fi.yml new file mode 100644 index 000000000..832cf6fd9 --- /dev/null +++ b/kompassi/events/kotaeexpo2026/forms/expense-claim-fi.yml @@ -0,0 +1,58 @@ +title: Hae kulukorvausta +description: | + Tällä lomakkeella voit hakea kulukorvausta Kotae ry:ltä tapahtumaan tai + yhdistystoimintaan liittyvästä kulusta. Täytä lomake huolellisesti ja liitä + mukaan kaikki pyydetyt liitteet. + + Jos et ole kysynyt etukäteen lupaa kuluille tai jos sinulla on kysyttävää + kulukorvauksista, ota yhteyttä sähköpostitse osoitteella talous ät kotae + piste fi tai Discordissa @Nimu tai @Aketzu. + +fields: + - slug: title + type: SingleLineText + title: Otsikko + required: true + helpText: Kerro lyhyesti, mistä kulusta haet korvausta tai mitä lasku koskee. + + - slug: description + type: MultiLineText + title: Kuvaus + helpText: | + Jos otsikko ei kerro kaikkea olennaista, voit antaa tässä lisätietoja. + + - slug: amount + type: DecimalField + minValue: 0 + decimalPlaces: 2 + title: Summa + required: true + helpText: | + Kuinka paljon haet korvausta tai mikä on laskun summa? Kirjoita summa euroina. + + - slug: recipient + type: SingleLineText + title: Saaja + required: true + helpText: | + Kenen tilille korvaus maksetaan? Kirjoita tähän etu- ja sukunimi tai yrityksen nimi. + + - slug: recipient_iban + type: SingleLineText + title: Saajan tilinumero + required: true + helpText: | + Mille tilille korvaus maksetaan? Kirjoita IBAN-tilinumero muodossa FI12 3456 7890 1234 56. + + - slug: attachments + type: FileUpload + title: Tositteet + required: true + multiple: true + helpText: | + Liitä mukaan kaikki kulukorvauksen kannalta tarpeelliset tositteet. + Tositteista tulee käydä ilmi kulun päivämäärä, summa, ALV-kanta ja maksun saaja. + Muussa valuutassa kuin euroissa tehdyistä maksuista tulee käydä ilmi myös valuuttakurssi tai + summa alkuperäisessä valuutassa. + Jos sinulla ei ole tositetta, kirjoita vapaamuotoinen selvitys kulusta ja + siitä, miksi tositetta ei ole saatavilla, ja allekirjoita se. diff --git a/kompassi/events/kotaeexpo2026/management/__init__.py b/kompassi/events/kotaeexpo2026/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kompassi/events/kotaeexpo2026/management/commands/__init__.py b/kompassi/events/kotaeexpo2026/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kompassi/events/kotaeexpo2026/management/commands/setup_kotaeexpo2026.py b/kompassi/events/kotaeexpo2026/management/commands/setup_kotaeexpo2026.py new file mode 100644 index 000000000..e29671fca --- /dev/null +++ b/kompassi/events/kotaeexpo2026/management/commands/setup_kotaeexpo2026.py @@ -0,0 +1,382 @@ +from datetime import datetime, timedelta + +from dateutil.tz import tzlocal +from django.conf import settings +from django.contrib.auth.models import Group +from django.contrib.contenttypes.models import ContentType +from django.core.management.base import BaseCommand +from django.utils.timezone import now + +from kompassi.access.models.email_alias_domain import EmailAliasDomain +from kompassi.access.models.email_alias_type import EmailAliasVariant +from kompassi.access.models.group_email_alias_grant import GroupEmailAliasGrant +from kompassi.badges.models import BadgesEventMeta +from kompassi.core.models import Event, Organization, Venue +from kompassi.forms.models.meta import FormsEventMeta +from kompassi.forms.models.survey import SurveyDTO +from kompassi.intra.models import IntraEventMeta, Team +from kompassi.involvement.models.involvement_to_badge import InvolvementToBadgeMapping +from kompassi.involvement.models.involvement_to_group import InvolvementToGroupMapping +from kompassi.involvement.models.meta import InvolvementEventMeta +from kompassi.involvement.models.registry import Registry +from kompassi.labour.models import AlternativeSignupForm, JobCategory, LabourEventMeta, PersonnelClass, Survey +from kompassi.labour.models.qualifications import Qualification +from kompassi.program_v2.models.meta import ProgramV2EventMeta + +from ...models import Accommodation, KnownLanguage, SignupExtra, WorkDay + + +class Setup: + def __init__(self): + self._ordering = 0 + + def get_ordering_number(self): + self._ordering += 10 + return self._ordering + + def setup(self, test=False): + self.test = test + self.tz = tzlocal() + self.setup_core() + self.setup_labour() + self.setup_badges() + self.setup_intra() + self.setup_access() + self.setup_forms() + self.setup_program_v2() + + def setup_core(self): + self.organization, unused = Organization.objects.get_or_create( + slug="kotae-ry", + defaults=dict( + name="Kotae ry", + homepage_url="https://www.kotae.fi", + ), + ) + self.venue, unused = Venue.objects.get_or_create(name="Tampereen Messu- ja Urheilukeskus") + self.event, unused = Event.objects.get_or_create( + slug="kotaeexpo2026", + defaults=dict( + name="Kotae Expo (2026)", + name_genitive="Kotae Expon", + name_illative="Kotae Expoon", + name_inessive="Kotae Expossa", + homepage_url="http://www.kotae.fi", + organization=self.organization, + start_time=datetime(2026, 11, 21, 10, 0, tzinfo=self.tz), + end_time=datetime(2026, 11, 22, 18, 0, tzinfo=self.tz), + venue=self.venue, + ), + ) + + def setup_labour(self): + (labour_admin_group,) = LabourEventMeta.get_or_create_groups(self.event, ["admins"]) + + content_type = ContentType.objects.get_for_model(SignupExtra) + + labour_event_meta_defaults = dict( + signup_extra_content_type=content_type, + work_begins=self.event.start_time.replace(day=20, hour=8, minute=0, tzinfo=self.tz), + work_ends=self.event.end_time.replace(hour=22, minute=0, tzinfo=self.tz), + admin_group=labour_admin_group, + contact_email="Kotae Expon vapaaehtoistiimi ", + ) + + if self.test: + t = now() + labour_event_meta_defaults.update( + registration_opens=t - timedelta(days=60), # type: ignore + registration_closes=t + timedelta(days=60), # type: ignore + ) + else: + pass + + labour_event_meta, unused = LabourEventMeta.objects.get_or_create( + event=self.event, + defaults=labour_event_meta_defaults, + ) + + for pc_name, pc_slug, pc_app_label in [ + ("Vastaava", "vastaava", "labour"), + ("Vapaaehtoinen", "vapaaehtoinen", "labour"), + ("Ohjelmanjärjestäjä", "ohjelma", "program_v2"), + ("Vieras", "vieras", "badges"), + # ("Ohjelmanpitäjä lauantai", "ohjelmanpitaja-la", "program_v2"), + # ("Ohjelmanpitäjä sunnuntai", "ohjelmanpitaja-su", "program_v2"), + # ("Ohjelmanpitäjä viikonloppu", "ohjelmanpitaja-viikonloppu", "program_v2"), + # ("Artesaani", "artesaani", "badges"), + # ("Taidekuja lauantai", "taidekuja-la", "badges"), + # ("Taidekuja sunnuntai", "taidekuja-su", "badges"), + # ("Expo näytteilleasettaja", "exponaytteilleasettaja", "badges"), + # ("Näytteilleasettaja", "naytteilleasettaja", "badges"), + ]: + PersonnelClass.objects.update_or_create( + event=self.event, + slug=pc_slug, + defaults=dict( + name=pc_name, + app_label=pc_app_label, + priority=self.get_ordering_number(), + ), + ) + + if not JobCategory.objects.filter(event=self.event).exists(): + previous_event = Event.objects.filter(slug=f"kotaeexpo{self.event.start_time.year - 1}").first() + if previous_event: + JobCategory.copy_from_event(source_event=previous_event, target_event=self.event) + + # Ensure required job categories exist + vapaaehtoinen = PersonnelClass.objects.get(event=self.event, slug="vapaaehtoinen") + vastaava = PersonnelClass.objects.get(event=self.event, slug="vastaava") + for jc_data in [ + ("vastaava", "Vastaava", "Tapahtuman järjestelytoimikunnan jäsen", [vastaava]), + ( + "jarjestyksenvalvoja", + "Järjestyksenvalvoja", + "Järjestyksenvalvojat valvovat kävijöiden turvallisuutta tapahtumassa. Tehtävä edellyttää täysi-ikäisyyttä, voimassa olevaa JV-korttia ja asiakaspalveluasennetta.", + [vapaaehtoinen], + ), + ]: + slug, name, description, pcs = jc_data + + job_category, created = JobCategory.objects.get_or_create( + event=self.event, + slug=slug, + defaults=dict( + name=name, + description=description, + ), + ) + + if created: + job_category.personnel_classes.set(pcs) + + for slug in ["vastaava"]: + JobCategory.objects.filter(event=self.event, slug=slug).update(public=False) + + for jc_slug, qualification_name in [ + ("jarjestyksenvalvoja", "JV-kortti"), + ]: + jc = JobCategory.objects.get(event=self.event, slug=jc_slug) + qual = Qualification.objects.get(name=qualification_name) + + jc.required_qualifications.set([qual]) + + labour_event_meta.create_groups() + + for language in [ + "Suomi", + "Ruotsi", + "Englanti", + "Japani", + "Korea", + ]: + KnownLanguage.objects.get_or_create(name=language) + + for accommodation_name in [ + "Pe-la väliselle yölle", + "La-su väliselle yölle", + ]: + Accommodation.objects.get_or_create(name=accommodation_name) + + for workday_name in [ + "Tapahtumaviikon keskiviikko ja torstai (logistiikka)", + "Perjantai", + "Lauantai", + "Sunnuntai", + "Tapahtuman jälkeinen maanantai ja tiistai (logistiikka)", + ]: + WorkDay.objects.get_or_create(name=workday_name) + + AlternativeSignupForm.objects.get_or_create( + event=self.event, + slug="vastaava", + defaults=dict( + title="Vastaavien ilmoittautumislomake", + signup_form_class_path="events.kotaeexpo2026.forms:OrganizerSignupForm", + signup_extra_form_class_path="events.kotaeexpo2026.forms:OrganizerSignupExtraForm", + active_from=now(), + active_until=self.event.end_time, + ), + ) + + AlternativeSignupForm.objects.get_or_create( + event=self.event, + slug="erikoistehtava", + defaults=dict( + title="Erikoistehtävien ilmoittautumislomake", + signup_form_class_path="events.kotaeexpo2026.forms:SpecialistSignupForm", + signup_extra_form_class_path="events.kotaeexpo2026.forms:SpecialistSignupExtraForm", + active_from=self.event.created_at, + active_until=self.event.start_time, + signup_message=( + "Täytä tämä lomake vain, " + "jos joku Kotae Expon vastaavista on ohjeistanut sinua ilmoittautumaan tällä lomakkeella. " + ), + ), + ) + + Survey.objects.get_or_create( + event=self.event, + slug="tyovuorotoiveet", + defaults=dict( + title="Työvuorotoiveet", + description=( + "Tässä vaiheessa voit vaikuttaa työvuoroihisi. Jos saavut tapahtumaan vasta sen alkamisen " + "jälkeen tai sinun täytyy lähteä ennen tapahtuman loppumista, kerro se tässä. Lisäksi jos " + "tiedät ettet ole käytettävissä tiettyihin aikoihin tapahtuman aikana tai haluat esimerkiksi " + "nähdä jonkun ohjelmanumeron, kerro siitäkin. Työvuorotoiveiden toteutumista täysin ei voida " + "taata." + ), + form_class_path="events.kotaeexpo2026.forms:ShiftWishesSurvey", + active_from=now(), + active_until=self.event.start_time - timedelta(days=60), + ), + ) + + def setup_badges(self): + (badge_admin_group,) = BadgesEventMeta.get_or_create_groups(self.event, ["admins"]) + meta, unused = BadgesEventMeta.objects.get_or_create( + event=self.event, + defaults=dict( + admin_group=badge_admin_group, + real_name_must_be_visible=False, + ), + ) + + def setup_access(self): + cc_group = self.event.labour_event_meta.get_group("vastaava") + domain = EmailAliasDomain.objects.get(domain_name="kotae.fi") + GroupEmailAliasGrant.ensure( + cc_group, + domain, + [ + EmailAliasVariant.FIRSTNAME_LASTNAME, + EmailAliasVariant.CUSTOM, + ], + ) + + def setup_intra(self): + (admin_group,) = IntraEventMeta.get_or_create_groups(self.event, ["admins"]) + organizer_group = self.event.labour_event_meta.get_group("vastaava") + IntraEventMeta.objects.update_or_create( + event=self.event, + defaults=dict( + admin_group=admin_group, + organizer_group=organizer_group, + is_organizer_list_public=True, + ), + ) + + for team_slug, team_name in [ + ("pj", "Pääjärjestäjä"), + ("grafiikka", "Grafiikka"), + ("kunniavieraat", "Kunniavieraat"), + ("markkinointi", "Markkinointi"), + ("ohjelma", "Ohjelma"), + ("talous", "Talous"), + ("taltiointi", "Taltiointi"), + ("tapahtumakehitys", "Tapahtumakehitys- ja laatu"), + ("tekniikka", "Tekniikka"), + ("tilat", "Tilat"), + ("turvallisuus", "Turvallisuus"), + ("vapaaehtoiset", "Vapaaehtoiset"), + ("viestinta", "Viestintä"), + ]: + (team_group,) = IntraEventMeta.get_or_create_groups(self.event, [team_slug]) + email = f"{team_slug}@kotae.fi" + + team, _ = Team.objects.get_or_create( + event=self.event, + slug=team_slug, + defaults=dict( + name=team_name, + order=self.get_ordering_number(), + group=team_group, + email=email, + ), + ) + + for team in Team.objects.filter(event=self.event): + team.is_public = True + team.save() + + def setup_forms(self): + (admin_group,) = FormsEventMeta.get_or_create_groups(self.event, ["admins"]) + + FormsEventMeta.objects.get_or_create( + event=self.event, + defaults=dict( + admin_group=admin_group, + ), + ) + + for survey in [ + SurveyDTO( + slug="expense-claim", + key_fields=["title", "amount"], + login_required=True, + anonymity="NAME_AND_EMAIL", + active_from=datetime(2026, 1, 1, 0, 0, tzinfo=self.tz), + active_until=datetime(2026, 12, 31, 23, 59, tzinfo=self.tz), + ), + ]: + survey.save(self.event) + + def setup_program_v2(self): + InvolvementEventMeta.ensure(self.event) + + (admin_group,) = ProgramV2EventMeta.get_or_create_groups(self.event, ["admins"]) + + # TODO(Kotae Expo): Define your volunteer registry + registry, _created = Registry.objects.get_or_create( + scope=self.organization.scope, + slug="volunteers", + defaults=dict( + title_fi="Kotae ry:n vapaaehtoisrekisteri", + title_en="Volunteers of Kotae ry", + ), + ) + + ProgramV2EventMeta.objects.update_or_create( + event=self.event, + defaults=dict( + admin_group=admin_group, + default_registry=registry, + contact_email="Kotae Expon ohjelmatiimi ", + ), + ) + + universe = self.event.involvement_universe + + ohjelma = PersonnelClass.objects.get(event=self.event, slug="ohjelma") + InvolvementToBadgeMapping.objects.update_or_create( + universe=universe, + personnel_class=ohjelma, + defaults=dict( + required_dimensions={ + "state": ["active"], + "type": ["program-host"], + }, + job_title="Ohjelmanjärjestäjä", + priority=self.get_ordering_number(), + ), + ) + + group, _ = Group.objects.get_or_create(name=f"{self.event.slug}-program-hosts") + InvolvementToGroupMapping.objects.get_or_create( + universe=universe, + required_dimensions={ + "state": ["active"], + "type": ["program-host"], + }, + group=group, + ) + + +class Command(BaseCommand): + args = "" + help = "Setup Kotae Expo 2026 specific stuff" + + def handle(self, *args, **opts): + Setup().setup(test=settings.DEBUG) diff --git a/kompassi/events/kotaeexpo2026/migrations/0001_initial.py b/kompassi/events/kotaeexpo2026/migrations/0001_initial.py new file mode 100644 index 000000000..0e9bacacb --- /dev/null +++ b/kompassi/events/kotaeexpo2026/migrations/0001_initial.py @@ -0,0 +1,220 @@ +# Generated by Django 6.0.1 on 2026-02-01 19:33 + +import django.core.validators +import django.db.models.deletion +from django.db import migrations, models + +import kompassi.labour.models.signup_extras + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("core", "0043_emailverificationtoken_language_and_more"), + ("enrollment", "0009_alter_enrollment_is_public_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="AccessibilityWarning", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=63)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="Accommodation", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=63)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="KnownLanguage", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=63)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="TimeSlot", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=63)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="WorkDay", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=63)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="SignupExtra", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("is_active", models.BooleanField(default=True)), + ( + "shift_type", + models.CharField( + choices=[ + ("yksipitka", "Yksi pitkä vuoro"), + ("montalyhytta", "Monta lyhyempää vuoroa"), + ("kaikkikay", "Kumpi tahansa käy"), + ], + help_text="Haluatko tehdä yhden pitkän työvuoron vaiko monta lyhyempää vuoroa?", + max_length=15, + verbose_name="Toivottu työvuoron pituus", + ), + ), + ( + "total_work", + models.CharField( + choices=[("8h", "8 tuntia - Täysvuoro"), ("yli10h", "Yli 10 tuntia - Supervuoro!")], + help_text="Kuinka paljon haluat tehdä töitä yhteensä tapahtuman aikana? Huomaathan, ettei 4 tunnin työpanos oikeuta täysiin työvoimaetuihin. Tarkasta työvoimaedut Kotaen verkkosivuilta.", + max_length=15, + verbose_name="Toivottu kokonaistyömäärä", + ), + ), + ( + "night_shift", + models.BooleanField(default=False, verbose_name="Olen valmis tekemään yötöitä klo 23-07"), + ), + ( + "overseer", + models.BooleanField( + default=False, + help_text="Vuorovastaavat ovat kokeneempia conityöläisiä, jotka toimivat oman tehtäväalueensa tiiminvetäjänä.", + verbose_name="Olen kiinnostunut vuorovastaavan tehtävistä", + ), + ), + ( + "want_certificate", + models.BooleanField(default=False, verbose_name="Haluan todistuksen työskentelystäni"), + ), + ("known_language_other", models.TextField(blank=True, verbose_name="Muu kieli")), + ( + "special_diet_other", + models.TextField( + blank=True, + help_text="Jos noudatat erikoisruokavaliota, jota ei ole yllä olevassa listassa, ilmoita se tässä. Tapahtuman järjestäjä pyrkii ottamaan erikoisruokavaliot huomioon, mutta kaikkia erikoisruokavalioita ei välttämättä pystytä järjestämään.", + verbose_name="Muu erikoisruokavalio", + ), + ), + ( + "prior_experience", + models.TextField( + blank=True, + help_text="Kerro tässä kentässä, jos sinulla on aiempaa kokemusta vastaavista tehtävistä tai muuta sellaista työkokemusta, josta arvioit olevan hyötyä hakemassasi tehtävässä.", + verbose_name="Työkokemus", + ), + ), + ( + "free_text", + models.TextField( + blank=True, + help_text="Jos haluat sanoa hakemuksesi käsittelijöille jotain sellaista, jolle ei ole omaa kenttää yllä, käytä tätä kenttää.", + verbose_name="Vapaa alue", + ), + ), + ( + "shift_wishes", + models.TextField( + blank=True, + help_text="Jos tiedät, ettet pääse paikalle johonkin tiettyyn aikaan tai haluat esimerkiksi osallistua johonkin tiettyyn ohjelmanumeroon, mainitse siitä tässä. HUOM! Muistathan mainita kellonajat (myös ohjelmanumeroista).", + verbose_name="Työvuorotoiveet", + ), + ), + ( + "email_alias", + models.CharField( + blank=True, + default="", + help_text="Coniitit saavat käyttöönsä nick@kotae.fi-tyyppisen sähköpostialiaksen, joka ohjataan coniitin omaan sähköpostilaatikkoon. Tässä voit toivoa haluamaasi sähköpostialiaksen alkuosaa eli sitä, joka tulee ennen @kotae.fi:tä. Sallittuja merkkejä ovat pienet kirjaimet a-z, numerot 0-9 sekä väliviiva.", + max_length=32, + validators=[ + django.core.validators.RegexValidator( + message="Tekninen nimi saa sisältää vain pieniä kirjaimia, numeroita sekä väliviivoja.", + regex="^[a-z0-9-]+$", + ) + ], + verbose_name="Sähköpostialias", + ), + ), + ( + "accommodation", + models.ManyToManyField( + blank=True, + related_name="kotaeexpo2026_signup_extras", + to="kotaeexpo2026.accommodation", + verbose_name="Tarvitsen lattiamajoitusta", + ), + ), + ( + "event", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_signup_extras", + to="core.event", + ), + ), + ( + "known_language", + models.ManyToManyField( + blank=True, + related_name="kotaeexpo2026_signup_extras", + to="kotaeexpo2026.knownlanguage", + verbose_name="Osaamasi kielet", + ), + ), + ( + "person", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_signup_extra", + to="core.person", + ), + ), + ( + "special_diet", + models.ManyToManyField( + blank=True, + related_name="kotaeexpo2026_signup_extras", + to="enrollment.specialdiet", + verbose_name="Erikoisruokavalio", + ), + ), + ( + "work_days", + models.ManyToManyField( + blank=True, + help_text="Merkitse ne päivät, jolloin olet käytettävissä vapaaehtoisena.", + related_name="kotaeexpo2026_signup_extras", + to="kotaeexpo2026.workday", + verbose_name="Työskentelypäivät", + ), + ), + ], + options={ + "abstract": False, + }, + bases=(kompassi.labour.models.signup_extras.SignupExtraMixin, models.Model), + ), + ] diff --git a/kompassi/events/kotaeexpo2026/migrations/__init__.py b/kompassi/events/kotaeexpo2026/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kompassi/events/kotaeexpo2026/models.py b/kompassi/events/kotaeexpo2026/models.py new file mode 100644 index 000000000..1bcc69a73 --- /dev/null +++ b/kompassi/events/kotaeexpo2026/models.py @@ -0,0 +1,171 @@ +from django.db import models + +from kompassi.core.utils import validate_slug +from kompassi.labour.models import SignupExtraBase +from kompassi.zombies.enrollment.models import SimpleChoice, SpecialDiet + +SHIFT_TYPE_CHOICES = [ + ("yksipitka", "Yksi pitkä vuoro"), + ("montalyhytta", "Monta lyhyempää vuoroa"), + ("kaikkikay", "Kumpi tahansa käy"), +] + +TOTAL_WORK_CHOICES = [ + ("8h", "8 tuntia - Täysvuoro"), + ("yli10h", "Yli 10 tuntia - Supervuoro!"), +] + + +class Accommodation(SimpleChoice): + pass + + +class WorkDay(SimpleChoice): + pass + + +class KnownLanguage(SimpleChoice): + pass + + +class SignupExtra(SignupExtraBase): + shift_type = models.CharField( + max_length=15, + verbose_name="Toivottu työvuoron pituus", + help_text="Haluatko tehdä yhden pitkän työvuoron vaiko monta lyhyempää vuoroa?", + choices=SHIFT_TYPE_CHOICES, + ) + + total_work = models.CharField( + max_length=15, + verbose_name="Toivottu kokonaistyömäärä", + help_text=( + "Kuinka paljon haluat tehdä töitä yhteensä tapahtuman aikana? Huomaathan, ettei 4 tunnin työpanos oikeuta täysiin työvoimaetuihin. Tarkasta työvoimaedut Kotaen verkkosivuilta." + ), + choices=TOTAL_WORK_CHOICES, + ) + + night_shift = models.BooleanField( + default=False, + verbose_name="Olen valmis tekemään yötöitä klo 23-07", + ) + + overseer = models.BooleanField( + default=False, + verbose_name="Olen kiinnostunut vuorovastaavan tehtävistä", + help_text=( + "Vuorovastaavat ovat kokeneempia conityöläisiä, jotka toimivat oman tehtäväalueensa tiiminvetäjänä." + ), + ) + + work_days = models.ManyToManyField( + WorkDay, + blank=True, + verbose_name="Työskentelypäivät", + help_text=("Merkitse ne päivät, jolloin olet käytettävissä vapaaehtoisena."), + related_name="kotaeexpo2026_signup_extras", + ) + + want_certificate = models.BooleanField( + default=False, + verbose_name="Haluan todistuksen työskentelystäni", + ) + + known_language = models.ManyToManyField( + KnownLanguage, + blank=True, + verbose_name="Osaamasi kielet", + related_name="kotaeexpo2026_signup_extras", + ) + + known_language_other = models.TextField( + blank=True, + verbose_name="Muu kieli", + ) + + special_diet = models.ManyToManyField( + SpecialDiet, + blank=True, + verbose_name="Erikoisruokavalio", + related_name="kotaeexpo2026_signup_extras", + ) + + special_diet_other = models.TextField( + blank=True, + verbose_name="Muu erikoisruokavalio", + help_text=( + "Jos noudatat erikoisruokavaliota, jota ei ole yllä olevassa listassa, " + "ilmoita se tässä. Tapahtuman järjestäjä pyrkii ottamaan erikoisruokavaliot " + "huomioon, mutta kaikkia erikoisruokavalioita ei välttämättä pystytä järjestämään." + ), + ) + + accommodation = models.ManyToManyField( + Accommodation, + blank=True, + verbose_name="Tarvitsen lattiamajoitusta", + related_name="kotaeexpo2026_signup_extras", + ) + + prior_experience = models.TextField( + blank=True, + verbose_name="Työkokemus", + help_text=( + "Kerro tässä kentässä, jos sinulla on aiempaa kokemusta vastaavista " + "tehtävistä tai muuta sellaista työkokemusta, josta arvioit olevan hyötyä " + "hakemassasi tehtävässä." + ), + ) + + free_text = models.TextField( + blank=True, + verbose_name="Vapaa alue", + help_text=( + "Jos haluat sanoa hakemuksesi käsittelijöille jotain sellaista, jolle ei ole " + "omaa kenttää yllä, käytä tätä kenttää." + ), + ) + + shift_wishes = models.TextField( + blank=True, + verbose_name="Työvuorotoiveet", + help_text=( + "Jos tiedät, ettet pääse paikalle johonkin tiettyyn aikaan tai haluat esimerkiksi " + "osallistua johonkin tiettyyn ohjelmanumeroon, mainitse siitä tässä. HUOM! Muistathan " + "mainita kellonajat (myös ohjelmanumeroista)." + ), + ) + + email_alias = models.CharField( + blank=True, + default="", + max_length=32, + verbose_name="Sähköpostialias", + help_text=( + "Coniitit saavat käyttöönsä nick@kotae.fi-tyyppisen sähköpostialiaksen, joka " + "ohjataan coniitin omaan sähköpostilaatikkoon. Tässä voit toivoa haluamaasi " + "sähköpostialiaksen alkuosaa eli sitä, joka tulee ennen @kotae.fi:tä. " + "Sallittuja merkkejä ovat pienet kirjaimet a-z, numerot 0-9 sekä väliviiva." + ), + validators=[validate_slug], + ) + + @classmethod + def get_form_class(cls): + from .forms import SignupExtraForm + + return SignupExtraForm + + @classmethod + def get_programme_form_class(cls): + from .forms import ProgrammeSignupExtraForm + + return ProgrammeSignupExtraForm + + +class TimeSlot(SimpleChoice): + pass + + +class AccessibilityWarning(SimpleChoice): + pass diff --git a/kompassi/settings.py b/kompassi/settings.py index be019fefd..7a16af347 100644 --- a/kompassi/settings.py +++ b/kompassi/settings.py @@ -286,6 +286,7 @@ "kompassi.events.kuplii2026", "kompassi.events.desucon2026", "kompassi.events.tracon2026", + "kompassi.events.kotaeexpo2026", # zombies are obsolete apps that can't be removed due to cross-app references in models "kompassi.zombies.enrollment", "kompassi.zombies.event_log",