Skip to content

Implement easy import of schools from the ME #248

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions oioioi/oi/files/rspo.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Numer RSPO;REGON;NIP;Typ;Nazwa;Kod terytorialny województwo;Kod terytorialny powiat;Kod terytorialny gmina;Kod terytorialny miejscowość;Kod terytorialny ulica;Województwo;Powiat;Gmina;Miejscowość;Rodzaj miejscowości;Ulica;Numer budynku;Numer lokalu;Kod pocztowy;Poczta;Telefon;Faks;E-mail;Strona www;Publiczność status;Kategoria uczniów;Specyfika placówki;Imię i nazwisko dyrektora;Data założenia;Data rozpoczęcia działalności;Data likwidacji;Typ organu prowadzącego;Nazwa organu prowadzącego;REGON organu prowadzącego;NIP organu prowadzącego;Województwo organu prowadzącego;Powiat organu prowadzącego;Gmina organu prowadzącego;Miejsce w strukturze;RSPO podmiotu nadrzędnego;Typ podmiotu nadrzędnego;Nazwa podmiotu nadrzędnego;Liczba uczniów;Tereny sportowe;Języki nauczane;Czy zatrudnia logopedę;Czy zatrudnia psychologa;Czy zatrudnia pedagoga;Oddziały podstawowe wg specyfiki;Oddziały dodatkowe
76;532397913;7531676389;Liceum ogólnokształcące;EUROPEJSKA SZKOŁA PODSTAWOWA W KIELCACH;02;0262;0262011;0954047;19998;DOLNOŚLĄSKIE;Legnica;Legnica;Legnica;miasto;ul. Fryderyka Skarbka;4;;59-220;Legnica;768523705;768523705;zsemleg1@wp.pl;www.zsem.legnica.pl;publiczna;Dzieci lub młodzież;brak specyfiki; ;31.08.2002;31.08.2002;;Miasto na prawach powiatu;GMINA LEGNICA;390647251;;DOLNOŚLĄSKIE;Legnica;Legnica;szkoła/placówka wchodząca w skład jednostki złożonej;48856;Zespół szkół i placówek oświatowych;ZESPÓŁ SZKÓŁ ELEKTRYCZNO-MECHANICZNYCH W LEGNICY;620;;angielski,niemiecki;Nie;Nie;Tak;ogólnodostępny;
79;532397563;7471067208;Liceum ogólnokształcące;I LICEUM OGÓLNOKSZTAŁCĄCE IM. BOLESŁAWA CHROBREGO W BRZEGU;16;1601;1601011;0965252;00432;OPOLSKIE;brzeski;Brzeg;Brzeg;miasto;ul. Armii Krajowej;7;;49-300;Brzeg;774163625;774242143;lo1brzeg@wodip.opole.pl;lo1brzeg.szkolnastrona.pl;publiczna;Dzieci lub młodzież;brak specyfiki;Katarzyna Grochowska;01.09.1945;01.09.1945;;Powiat ziemski;POWIAT BRZESKI;531412444;7471567388;OPOLSKIE;brzeski;Brzeg;samodzielna;;;;348;boiska do siatkówki,boiska do koszykówki,boiska do piłki ręcznej,rzutnie;angielski,niemiecki,włoski;Nie;Tak;Tak;ogólnodostępny;
51;000735055;5431021630;Technikum;TECHNIKUM NR 5 W LEGNICY;20;2003;2003011;0922685;11205;PODLASKIE;bielski;Bielsk Podlaski;Bielsk Podlaski;miasto;ul. 11 Listopada;6;;17-100;Bielsk Podlaski;858332673;;1lo_bielsk_podlaski@wp.pl;www.1lobielskpodlaski.edupage.org;publiczna;Dzieci lub młodzież;brak specyfiki;Marzena Pogorzelska-Ciołek;07.11.1918;10.02.1919;;Powiat ziemski;POWIAT BIELSKI;050658574;5432012248;PODLASKIE;bielski;Bielsk Podlaski;samodzielna;;;;367;;angielski,francuski,niemiecki,rosyjski;Nie;Tak;Tak;ogólnodostępny;
98;532400897;7471042148;Liceum ogólnokształcące;LICEUM OGÓLNOKSZTAŁCĄCE W KROŚNIEWICACH;16;1601;1601011;0965252;11926;OPOLSKIE;brzeski;Brzeg;Brzeg;miasto;ul. 1 Maja;7;;49-305;Brzeg;774111408;774111408;lo2brzeg@wodip.opole.pl;lo2brzeg.wodip.opole.pl;publiczna;Dzieci lub młodzież;brak specyfiki;Leszek Lipiński;01.09.1990;01.09.1990;;Powiat ziemski;POWIAT BRZESKI;531412444;7471567388;OPOLSKIE;brzeski;Brzeg;samodzielna;;;;397;boiska do piłki nożnej;angielski,francuski,hiszpański,niemiecki;Nie;Nie;Tak;ogólnodostępny,dwujęzyczny w szkole podstawowej, liceum i technikum;
101;146242052;8212636505;Szkoła podstawowa;SZKOŁA PODSTAWOWA NR 3 IM. DOKTORA JANUSZA PETERA W TOMASZOWIE LUBELSKIM;14;1426;1426092;0687570;21970;MAZOWIECKIE;siedlecki;Skórzec;Gołąbek;wieś;ul. Szkolna;26;;08-114;Gołąbek;256316682;;dyrektor@nspgolabek.pl;nspgolabek.pl;niepubliczna;Dzieci lub młodzież;brak specyfiki;EWA PIEKART;05.08.2012;31.08.2012;01.09.1945;Stowarzyszenia;STOWARZYSZENIE KULTURALNO-OŚWIATOWE TĘCZA;145856190;8212636505;MAZOWIECKIE;siedlecki;Skórzec;samodzielna;;;;77;boiska uniwersalne/wielozadaniowe;angielski,niemiecki;Tak;Nie;Tak;ogólnodostępny;
115;000273608;9211378556;Szkoła podstawowa;SZKOŁA PODSTAWOWA W BĘDZINIE Z SIEDZIBĄ W ŁEKNIE;06;0618;0618011;0988075;26608;LUBELSKIE;tomaszowski;Tomaszów Lubelski;Tomaszów Lubelski;miasto;ul. Żwirki i Wigury;6;;22-600;Tomaszów Lubelski;846642443;;spnr3tom@post.pl;www.spnr3tom.superszkolna.pl;publiczna;Dzieci lub młodzież;brak specyfiki;PIOTR SZUMILAK;31.08.1964;31.08.1964;;Gmina;MIASTO TOMASZÓW LUBELSKI;950369110;;LUBELSKIE;tomaszowski;Tomaszów Lubelski;samodzielna;;;;539;boiska do piłki nożnej,boiska uniwersalne/wielozadaniowe;angielski,niemiecki;Tak;Tak;Tak;ogólnodostępny,integracyjny,sportowy;
116;260627938;6572607539;Liceum ogólnokształcące;LICEUM OGÓLNOKSZTAŁCĄCE NR 3 WE WROCŁAWIU;26;2661;2661011;0945930;20291;ŚWIĘTOKRZYSKIE;Kielce;Kielce;Kielce;miasto;ul. Juliusza Słowackiego;5;;25-365;Kielce;413435199;;sekretariat@nazaret.kielce.pl;www.nazaret.kielce.pl;publiczna;Dzieci lub młodzież;brak specyfiki;MAŁGORZATA BIAŁEK;29.09.2003;01.09.2004;;Organizacje Wyznaniowe;ZGROMADZENIE SIÓSTR NAJŚWIĘTSZEJ RODZINY Z NAZARETU, PROWINCJA KRAKOWSKA;006228572;6792527340;MAŁOPOLSKIE;Kraków;Kraków-Podgórze;samodzielna;;;;317;;angielski,francuski,łacina,niemiecki,włoski;Nie;Tak;Tak;ogólnodostępny;Grupa nauczania języka mniejszości
23 changes: 23 additions & 0 deletions oioioi/oi/fixtures/test_schools_import.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[
{
"pk": 1,
"model": "oi.schooltype",
"fields": {
"name": "Szkoła podstawowa"
}
},
{
"pk": 2,
"model": "oi.schooltype",
"fields": {
"name": "Liceum ogólnokształcące"
}
},
{
"pk": 3,
"model": "oi.schooltype",
"fields": {
"name": "Technikum"
}
}
]
2 changes: 1 addition & 1 deletion oioioi/oi/forms.py
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
class AddSchoolForm(forms.ModelForm):
class Meta(object):
model = School
exclude = ['is_active', 'is_approved']
exclude = ['is_active', 'is_approved', 'rspo', 'type']


def city_options(province):
2 changes: 1 addition & 1 deletion oioioi/oi/management/commands/export_schools.py
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
from django.core.management.base import BaseCommand
from django.utils.translation import gettext as _

from oioioi.oi.management.commands.import_schools import COLUMNS
from oioioi.oi.management.commands.import_schools_legacy import COLUMNS
from oioioi.oi.models import School


2 changes: 1 addition & 1 deletion oioioi/oi/management/commands/export_schools_id.py
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
from django.core.management.base import BaseCommand
from django.utils.translation import gettext as _

from oioioi.oi.management.commands.import_schools import COLUMNS
from oioioi.oi.management.commands.import_schools_legacy import COLUMNS
from oioioi.oi.models import School

COLUMNS = ['id'] + COLUMNS
591 changes: 501 additions & 90 deletions oioioi/oi/management/commands/import_schools.py

Large diffs are not rendered by default.

122 changes: 122 additions & 0 deletions oioioi/oi/management/commands/import_schools_legacy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# ~*~ coding: utf-8 ~*~
import os
import string

import urllib.request
import unicodecsv
from django.core.exceptions import ValidationError
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.utils.translation import gettext as _

from oioioi.oi.models import School

COLUMNS = ['name', 'address', 'postal_code', 'city', 'province', 'phone', 'email']


class Command(BaseCommand):
columns_str = ', '.join(COLUMNS)

help = _(
"Updates the list of schools from the given CSV file "
"<filename or url>, with the following columns: %(columns)s.\n\n"
"Given CSV file should contain a header row with column names "
"(respectively %(columns)s) separated by commas. Following rows "
"should contain school data."
) % {'columns': columns_str}

requires_model_validation = True

def add_arguments(self, parser):
parser.add_argument('filename_or_url', type=str, help='Source CSV file')

def handle(self, *args, **options):
arg = options['filename_or_url']

if arg.startswith('http://') or arg.startswith('https://'):
self.stdout.write(_("Fetching %s...\n") % (arg,))
stream = urllib.request.urlopen(arg)
else:
if not os.path.exists(arg):
raise CommandError(_("File not found: %s") % arg)
stream = open(arg, 'rb')

reader = unicodecsv.DictReader(stream)
fields = reader.fieldnames
if fields != COLUMNS:
raise CommandError(
_("Missing header or invalid columns: %(h)s. Expected: %(col)s")
% {'h': ', '.join(fields), 'col': ', '.join(COLUMNS)}
)

with transaction.atomic():
ok = True
all_count = 0
created_count = 0
for row in reader:
all_count += 1

row['address'] = row['address'].replace('ul.', '')
row['address'] = row['address'].strip(' ')
row['address'] = string.capwords(row['address'])

row['postal_code'] = ''.join(row['postal_code'].split())

for hypen in (' - ', u'\u2010'):
row['city'] = row['city'].replace(hypen, '-')
row['city'] = row['city'].title()

row['province'] = row['province'].lower()

row['phone'] = row['phone'].split(',')[0]
row['phone'] = row['phone'].split(';')[0]
for c in ['tel.', 'fax.', '(', ')', '-', ' ']:
row['phone'] = row['phone'].replace(c, '')
row['phone'] = row['phone'].lstrip('0')

row['email'] = row['email'].split(',')[0]
row['email'] = row['email'].split(';')[0]

school, created = School.objects.get_or_create(
name=row['name'], postal_code=row['postal_code']
)
if created:
created_count += 1

for column in COLUMNS:
setattr(school, column, row[column])

school.is_active = True
school.is_approved = True

try:
school.full_clean()
school.save()
except ValidationError as e:
for k, v in e.message_dict.items():
for msg in v:
if k == '__all__':
self.stdout.write(
_("Line %(lineNum)s: %(msg)s\n")
% {'lineNum': reader.line_num, 'msg': msg}
)
else:
self.stdout.write(
_("Line %(lineNum)s, field %(field)s: %(msg)s\n")
% {
'lineNum': reader.line_num,
'field': k,
'msg': msg,
}
)
ok = False

if ok:
self.stdout.write(
_("Processed %(all_count)d entries (%(new_count)d new)\n")
% {'all_count': all_count, 'new_count': created_count}
)
else:
raise CommandError(
_("There were some errors. Database not changed\n")
)
45 changes: 45 additions & 0 deletions oioioi/oi/migrations/0007_schooltype_school_rspo_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Generated by Django 4.2.4 on 2023-08-17 10:29

from django.db import migrations, models
import django.db.models.deletion
import oioioi.base.utils.validators
import oioioi.participants.fields


def create_default_school_types(apps, schema_editor):
names = [
'Szkoła podstawowa',
'Liceum ogólnokształcące',
'Technikum'
]
SchoolType = apps.get_model('oi', 'SchoolType')
for name in names:
type = SchoolType(name=name)
type.save()

class Migration(migrations.Migration):

dependencies = [
('oi', '0006_auto_20210620_1806'),
]

operations = [
migrations.CreateModel(
name='SchoolType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, validators=[oioioi.base.utils.validators.validate_whitespaces], verbose_name='name')),
],
),
migrations.AddField(
model_name='school',
name='rspo',
field=models.PositiveIntegerField(blank=True, null=True, unique=True),
),
migrations.AddField(
model_name='school',
name='type',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='oi.schooltype'),
),
migrations.RunPython(create_default_school_types),
]
9 changes: 9 additions & 0 deletions oioioi/oi/models.py
Original file line number Diff line number Diff line change
@@ -62,7 +62,16 @@



class SchoolType(models.Model):
name = models.CharField(
max_length=255, validators=[validate_whitespaces], verbose_name=_("name")
)



class School(models.Model):
rspo = models.PositiveIntegerField(blank=True, null=True, unique=True)
type = models.ForeignKey(SchoolType, null=True, on_delete=models.SET_NULL)
name = models.CharField(
max_length=255, validators=[validate_whitespaces], verbose_name=_("name")
)
18 changes: 13 additions & 5 deletions oioioi/oi/tests.py
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
import os
import re
from datetime import datetime, timedelta, timezone # pylint: disable=E0611
from unittest.mock import patch

from django.contrib.admin.utils import quote
from django.contrib.auth.models import User
@@ -25,6 +26,7 @@ class TestOIAdmin(TestCase):
'test_contest',
'test_oi_registration',
'test_permissions',
'test_schools_import',
]

def test_admin_menu(self):
@@ -39,12 +41,18 @@ def test_admin_menu(self):
self.assertNotContains(response, 'Regions')

def test_schools_import(self):
filename = os.path.join(os.path.dirname(__file__), 'files', 'schools.csv')
# WARNING: There *cannot be* oioioi/schools directory automatically generated and deleted by the script.
patch('builtins.input', return_value='y')
filename = os.path.join(os.path.dirname(__file__), 'files', 'rspo.csv')
manager = import_schools.Command()
manager.run_from_argv(['manage.py', 'import_schools', filename])
self.assertEqual(School.objects.count(), 3)
school = School.objects.get(postal_code='02-044')
self.assertEqual(school.city, u'Bielsko-Biała Zdrój')
manager.run_from_argv(['manage.py', 'import_schools', filename, '--first-import'])
self.assertEqual(School.objects.count(), 7)
school = School.objects.get(postal_code='49-305')
self.assertEqual(school.city, u'Brzeg')
BASE_DIR = f'{os.getcwd()}/schools'
for file in os.listdir(BASE_DIR):
os.remove(os.path.join(BASE_DIR, file))
os.rmdir(BASE_DIR)

def test_safe_exec_mode(self):
contest = Contest.objects.get()