Commit b6b2248b authored by Thom Wiggers's avatar Thom Wiggers 📐

Merge branch '33-migrate-members' into 'master'

Resolve "Migrate members"

Closes #33

See merge request !103
parents 8434dd57 e70a2d0d
......@@ -26,7 +26,7 @@ class BoardAdmin(TranslatedModelAdmin):
@admin.register(models.CommitteeMembership)
class CommitteeMembershipAdmin(TranslatedModelAdmin):
pass
list_display = ('member', 'committee', 'since', 'until', 'chair', 'role')
@admin.register(models.Mentorship)
......
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-10-05 19:40
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('activemembers', '0009_translations'),
]
operations = [
migrations.RemoveField(
model_name='mentorship',
name='members',
),
migrations.DeleteModel(
name='Mentorship',
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-10-05 19:41
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('members', '0010_merge_20160907_2042'),
('activemembers', '0010_auto_20161005_2140'),
]
operations = [
migrations.CreateModel(
name='Mentorship',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('year', models.IntegerField(validators=django.core.validators.MinValueValidator(1990))),
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='members.Member', verbose_name='Member')),
],
),
migrations.AlterUniqueTogether(
name='mentorship',
unique_together=set([('member', 'year')]),
),
]
......@@ -3,6 +3,7 @@ import logging
from django.contrib.auth.models import Permission
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
from django.core.validators import MinValueValidator
from django.db import models
from django.urls import reverse
from django.utils import timezone
......@@ -159,14 +160,6 @@ class CommitteeMembership(models.Model, metaclass=ModelTranslateMeta):
null=True,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.pk is not None:
self._was_chair = bool(self.chair)
else:
self._was_chair = False
@property
def is_active(self):
"""Is this membership currently active"""
......@@ -220,38 +213,16 @@ class CommitteeMembership(models.Model, metaclass=ModelTranslateMeta):
'this period')})
def save(self, *args, **kwargs):
"""Save the instance"""
# If the chair changed and we're still active, we create a new instance
# Inactive instances should be handled manually
if (self.pk is not None and self._was_chair != self.chair and
not self.until and self.since != timezone.now().date()):
logger.info("Creating new membership instance")
self.until = timezone.now().date() - datetime.timedelta(days=1)
super().save(*args, **kwargs)
self.pk = None # forces INSERT
# Set since date to older expiration:
self.since = timezone.now().date()
self.until = None
self._was_chair = self.chair
super().save(*args, **kwargs)
self.member.user.is_staff = self.member.membership_set.exclude(
until__lt=timezone.now().date()).count() >= 1
self.member.user.save()
def delete(self, *args, **kwargs):
"""Deactivates active memberships, deletes inactive ones"""
if self.is_active:
self.until = timezone.now().date()
self.save()
else:
super().delete(*args, **kwargs)
def __str__(self):
return "{} membership of {} since {}".format(self.member,
self.committee,
self.since)
return "{} membership of {} since {}, until {}".format(self.member,
self.committee,
self.since,
self.until)
class Meta:
verbose_name = _('committee membership')
......@@ -259,8 +230,16 @@ class CommitteeMembership(models.Model, metaclass=ModelTranslateMeta):
class Mentorship(models.Model):
members = models.ManyToManyField(Member)
year = models.IntegerField(unique=True)
member = models.ForeignKey(
Member,
on_delete=models.CASCADE,
verbose_name=_('Member'),
)
year = models.IntegerField(validators=MinValueValidator(1990))
def __str__(self):
return _("Mentor introduction {year}").format(year=self.year)
return _("{name} mentor in {year}").format(name=self.member,
year=self.year)
class Meta:
unique_together = ('member', 'year')
......@@ -82,13 +82,6 @@ class CommitteeMembersTest(TestCase):
self.m.until = timezone.now().date().replace(year=1900)
self.assertFalse(self.m.is_active)
def test_delete(self):
self.m.delete()
self.assertIsNotNone(self.m.until)
self.assertIsNotNone(self.m.pk)
self.m.delete()
self.assertIsNone(self.m.pk)
class CommitteeMembersChairTest(TestCase):
fixtures = ['members.json', 'committees.json']
......@@ -125,25 +118,6 @@ class CommitteeMembersChairTest(TestCase):
self.m1.chair = True
self.m1.full_clean()
def test_change_chair(self):
pk = self.m1.pk
original_chair = self.m1.chair
self.m1.save()
self.assertEqual(self.m1.pk, pk, "new object created")
self.m1.chair = not original_chair
self.m1.save()
self.assertNotEqual(self.m1.pk, pk, "No new object created")
def test_change_chair_inactive(self):
pk = self.m1.pk
original_chair = self.m1.chair
self.m1.until = timezone.now().date()
self.m1.save()
self.assertEqual(self.m1.pk, pk, "new object created")
self.m1.chair = not original_chair
self.m1.save()
self.assertEqual(self.m1.pk, pk, "No new object created")
class PermissionsBackendTest(TestCase):
fixtures = ['members.json', 'committees.json']
......
from django.core.files.base import ContentFile
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.dateparse import parse_date
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
from django.utils.translation import activate
from activemembers.models import (Board, Committee, CommitteeMembership,
Mentorship)
from members.models import Member
from bs4 import BeautifulSoup
import requests
import json
import os
def imagefield_from_url(imagefield, url):
file = ContentFile(requests.get(url).content)
imagefield.save(os.path.basename(url), file)
class Command(BaseCommand):
help = "Migrates members and related committees / memberships"
def handle(self, *args, **options):
activate('en')
if not settings.MIGRATION_KEY:
raise ImproperlyConfigured("MIGRATION_KEY not specified")
url = "https://thalia.nu/api/members_export.php?apikey={}".format(
settings.MIGRATION_KEY
)
data = json.loads(requests.get(url).text)
groups = {}
mentorgroups = {}
board_url = "https://thalia.nu/board/2015-2016"
soup = BeautifulSoup(requests.get(board_url).text, 'lxml')
default_board_photo = ("https://thalia.nu/application/files/"
"6614/3560/3446/site_logo_board.png")
print("Migrating boards..")
for board in data['boards']:
obj, cr = Board.objects.get_or_create(name_nl=board['name'])
obj.name_en = board['name']
groups[board['gID']] = obj
img = soup.find("img", {"alt": board['name'][8:]})
if img:
imagefield_from_url(obj.photo, img['src'])
else:
imagefield_from_url(obj.photo, default_board_photo)
obj.save()
for mentorgroup in data['mentors']:
mentorgroups[mentorgroup['gID']] = int(mentorgroup['name'][-4:])
print("Migrating committees..")
committee_url = "https://thalia.nu/committees"
soup = BeautifulSoup(requests.get(committee_url).text, 'lxml')
anchors = soup.find('ul', {'class': 'row committees'}).find_all('a')
links = {anchor.find('h2').text: anchor['href'] for anchor in anchors}
for committee in data['committees']:
obj, cr = Committee.objects.get_or_create(
name_nl=committee['name'])
obj.name_en = committee['name']
groups[committee['gID']] = obj
obj.save()
if committee['name'] not in links:
continue
src = requests.get(links[committee['name']]).text
soup = BeautifulSoup(src, 'lxml')
div = soup.find('div', {'id': 'committee-div'})
obj.description_en = div.find('p').text
obj.description_nl = div.find('p').text
img = div.find('img')
imagefield_from_url(obj.photo, "https://thalia.nu" + img['src'])
for member in data['members']:
user, cr = User.objects.get_or_create(username=member['username'])
print("Migrating {}".format(member['username']))
user.username = member['username']
user.email = member['email']
# Concrete5 uses bcrypt passwords, which django can rehash
user.password = 'bcrypt$' + member['password']
user.first_name = member['first_name']
user.last_name = ' '.join([member['infix'], member['surname']])
user.save()
try:
user.member
except Member.DoesNotExist:
user.member = Member()
user.member.programme = {
'Computer Science': 'computingsience',
'Information Science': 'informationscience',
'Other': None,
'': None,
}[member['study']]
if member['student_number']:
user.member.student_number = member['student_number']
if member['member_since']:
# This is as best as we can do, although this may be incorrect
user.member.starting_year = member['member_since']
user.member.address_street = member['address1']
if member['address2']:
user.member.address_street2 = member['address2']
user.member.address_city = member['city']
user.member.address_postal_code = member['postalcode']
if member['mobile_number']:
user.member.phone_number = member['mobile_number']
elif member['phone_number']:
user.member.phone_number = member['phone_number']
if member['phone_number_parents']:
user.member.emergency_contact = '[default: Parents]'
user.member.emergency_contact_phone_number = (
member['phone_number_parents'])
if member['birthday']:
user.member.birthday = member['birthday'].split(' ')[0]
if user.member.birthday == "0000-00-00":
user.member.birthday = None
elif user.member.birthday[:3] == "201": # Likely incorrect!
user.member.birthday = None
user.member.show_birthday = bool(member['show_birthday'])
if member['website']:
user.member.website = member['website']
if member['about']:
user.member.profile_description = member['about']
if member['nickname']:
user.member.nickname = member['nickname']
if member['initials']:
user.member.initials = member['initials']
user.member.display_name_preference = {
'Full name': 'full',
'Initials and last name': 'initials',
'First name': 'firstname',
'Nickname': 'nickname',
'First name + nickname + last name': 'fullnick',
'Nickname + last name': 'nicklast',
'': 'full',
}[member['display_name']]
if member['avatar']:
imagefield_from_url(user.member.photo, member['avatar'])
if member['language']:
user.member.language = member['language']
user.member.receive_optin = bool(member['receive_optin_mail'])
user.member.direct_debit_authorized = (
member['payment_authorised'] == 'Authorised')
if member['payment_iban']:
user.member.bank_account = member['payment_iban']
user.member.save()
for membership in member['memberships']:
mdata = membership['membership']
if not mdata['begindate'] or mdata['begindate'][:4] == '0000':
mdata['begindate'] = '1970-01-01' # Manually fix this
for p in membership['presidencies'] + membership['roles']:
if not p['begindate'] or p['begindate'][:4] == '0000':
p['begindate'] = '1970-01-01'
if mdata['gID'] in mentorgroups:
m, cr = Mentorship.objects.get_or_create(
year=mentorgroups[mdata['gID']],
member=user.member)
m.save()
if mdata['gID'] not in groups:
continue # These are concrete5 groups (Admin, etc..)
group = groups[mdata['gID']]
dates = ([p['begindate'] for p in membership['presidencies']] +
[p['enddate'] for p in membership['presidencies']] +
[r['begindate'] for r in membership['roles']] +
[r['enddate'] for r in membership['roles']] +
[mdata['begindate']])
dates = set(dates)
try:
dates.remove(mdata['enddate'])
dates.remove(None)
except KeyError:
pass # Silence if enddate or None do not appear
if not dates:
dates = {'1970-01-01'} # Manually fix where this appears
newmship = None
for date in sorted(dates):
if newmship:
newmship.until = parse_date(date)
newmship.save()
newmship = CommitteeMembership()
newmship.member = user.member
newmship.committee = group
newmship.since = parse_date(date)
presidencies = [p for p in membership['presidencies']
if p['begindate'] >= date and
(not p['enddate'] or date < p['enddate'])]
if len(presidencies) >= 1:
newmship.chair = True
roles = [r['role'] for r in membership['roles']
if r['begindate'] >= date and
(not r['enddate'] or date < r['enddate'])]
if len(roles) >= 1:
newmship.role_nl = ' / '.join(roles)
newmship.role_en = ' / '.join(roles)
if mdata['enddate'] is not None:
if newmship.since != parse_date(mdata['enddate']):
newmship.until = parse_date(mdata['enddate'])
newmship.save()
else:
newmship.save()
for m in CommitteeMembership.objects.filter(member=user.member):
ms = (CommitteeMembership.objects
.filter(committee_id=m.committee_id,
member_id=m.member_id,
since=m.until,
chair=m.chair,
role_en=m.role_en,
role_nl=m.role_nl,
))
if not ms:
continue
if len(ms) > 1:
raise Exception("Could not merge more than one membership")
m.until = ms[0].until
m.save()
ms[0].delete()
print("Sanitizing board memberships")
for m in CommitteeMembership.objects.all():
try:
if m.committee.board:
m.since = parse_date(
'{}-09-01'.format(m.committee.name_nl[8:12]))
m.until = parse_date(
'{}-09-01'.format(m.committee.name_nl[13:17]))
m.save()
except Board.DoesNotExist:
pass
# remove duplicates to be sure
print("Cleaning up duplicates")
for m in CommitteeMembership.objects.all():
if (CommitteeMembership.objects
.filter(committee_id=m.committee_id,
member_id=m.member_id,
since=m.since,
chair=m.chair,
role_en=m.role_en,
role_nl=m.role_nl)
.count()) > 1:
m.delete()
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-09-01 19:59
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('members', '0006_auto_20160824_2041'),
]
operations = [
migrations.AlterField(
model_name='member',
name='profile_description',
field=models.TextField(blank=True, help_text='Text to display on your profile', null=True, verbose_name='Profile text'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-09-01 20:25
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('members', '0007_auto_20160901_2159'),
]
operations = [
migrations.AddField(
model_name='member',
name='initials',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Initials'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-09-01 20:44
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('members', '0008_member_initials'),
]
operations = [
migrations.AlterField(
model_name='member',
name='display_name_preference',
field=models.CharField(choices=[('full', 'Show full name'), ('nickname', 'Show only nickname'), ('firstname', 'Show only first name'), ('initials', 'Show initials and last name'), ('fullnick', 'Show name like "John \'nickname\' Doe"'), ('nicklast', 'Show nickname and last name')], default='full', max_length=10, verbose_name='How to display name'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-09-07 18:42
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('members', '0007_member_receive_newsletter'),
('members', '0009_auto_20160901_2244'),
]
operations = [
]
......@@ -216,6 +216,14 @@ class Member(models.Model):
verbose_name=_('Profile text'),
help_text=_('Text to display on your profile'),
blank=True,
null=True,
)
initials = models.CharField(
max_length=20,
verbose_name=_('Initials'),
blank=True,
null=True,
)
nickname = models.CharField(
......@@ -230,6 +238,7 @@ class Member(models.Model):
verbose_name=_('How to display name'),
choices=(('full', _('Show full name')),
('nickname', _('Show only nickname')),
('firstname', _('Show only first name')),
('initials', _('Show initials and last name')),
('fullnick', _("Show name like \"John 'nickname' Doe\"")),
('nicklast', _("Show nickname and last name"))),
......@@ -290,6 +299,8 @@ class Member(models.Model):
pref = self.display_name_preference
if pref == 'nickname':
return self.nickname
if pref == 'firstname':
return self.user.first_name
elif pref == 'initials':
return '{} {}'.format(self.initials, self.user.last_name)
elif pref == 'fullnick':
......
......@@ -48,3 +48,11 @@ COMPRESS_PRECOMPILERS = (
COMPRESS_CSS_FILTERS = ['compressor.filters.css_default.CssAbsoluteFilter',
'compressor.filters.cssmin.rCSSMinFilter']
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'django.contrib.auth.hashers.BCryptPasswordHasher',
]
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment