From d50b1a997f3d3b95b4c93089788c2bce5f985c05 Mon Sep 17 00:00:00 2001
From: "J.-S. Caux" <J.S.Caux@uva.nl>
Date: Tue, 19 Feb 2019 07:12:49 +0100
Subject: [PATCH] Create models organizations.ContactPerson, Contact,
 ContactRole

---
 organizations/constants.py                    | 11 +++
 .../0003_contact_contactperson_contactrole.py | 53 +++++++++++++
 organizations/models.py                       | 77 ++++++++++++++++++-
 partners/models.py                            |  1 +
 4 files changed, 141 insertions(+), 1 deletion(-)
 create mode 100644 organizations/migrations/0003_contact_contactperson_contactrole.py

diff --git a/organizations/constants.py b/organizations/constants.py
index 6a6056ac8..ba57d4e44 100644
--- a/organizations/constants.py
+++ b/organizations/constants.py
@@ -63,3 +63,14 @@ ORGANIZATION_STATUSES = (
     (ORGSTATUS_SUPERSEDED, 'Superseded'),
     (ORGSTATUS_OBSOLETE, 'Obsolete'),
 )
+
+ROLE_GENERAL = 'gen'
+ROLE_TECH = 'tech'
+ROLE_FIN = 'fin'
+ROLE_LEG = 'leg'
+ROLE_KINDS = (
+    (ROLE_GENERAL, 'General Contact'),
+    (ROLE_TECH, 'Technical Contact'),
+    (ROLE_FIN, 'Financial Contact'),
+    (ROLE_LEG, 'Legal Contact')
+)
diff --git a/organizations/migrations/0003_contact_contactperson_contactrole.py b/organizations/migrations/0003_contact_contactperson_contactrole.py
new file mode 100644
index 000000000..08ae732bb
--- /dev/null
+++ b/organizations/migrations/0003_contact_contactperson_contactrole.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2019-02-19 06:12
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+import scipost.fields
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('organizations', '0002_populate_from_partners_org'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Contact',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('title', models.CharField(choices=[('PR', 'Prof.'), ('DR', 'Dr'), ('MR', 'Mr'), ('MRS', 'Mrs'), ('MS', 'Ms')], max_length=4)),
+                ('activation_key', models.CharField(blank=True, max_length=40)),
+                ('key_expires', models.DateTimeField(default=django.utils.timezone.now)),
+                ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='org_contact', to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='ContactPerson',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('title', models.CharField(choices=[('PR', 'Prof.'), ('DR', 'Dr'), ('MR', 'Mr'), ('MRS', 'Mrs'), ('MS', 'Ms')], max_length=4)),
+                ('first_name', models.CharField(max_length=64)),
+                ('last_name', models.CharField(max_length=64)),
+                ('email', models.EmailField(max_length=254)),
+                ('role', models.CharField(max_length=128)),
+                ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='organizations.Organization')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='ContactRole',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('kind', scipost.fields.ChoiceArrayField(base_field=models.CharField(choices=[('gen', 'General Contact'), ('tech', 'Technical Contact'), ('fin', 'Financial Contact'), ('leg', 'Legal Contact')], max_length=4), size=None)),
+                ('date_from', models.DateField()),
+                ('date_until', models.DateField()),
+                ('contact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='roles', to='organizations.Contact')),
+                ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='organizations.Organization')),
+            ],
+        ),
+    ]
diff --git a/organizations/models.py b/organizations/models.py
index e2aded3db..fc5b30655 100644
--- a/organizations/models.py
+++ b/organizations/models.py
@@ -3,7 +3,11 @@ __license__ = "AGPL v3"
 
 
 import datetime
+import hashlib
+import random
+import string
 
+from django.contrib.auth.models import User
 from django.contrib.postgres.fields import JSONField
 from django.db import models
 from django.db.models import Sum
@@ -13,9 +17,12 @@ from django.urls import reverse
 from django_countries.fields import CountryField
 
 from .constants import ORGANIZATION_TYPES, ORGTYPE_PRIVATE_BENEFACTOR,\
-    ORGANIZATION_STATUSES, ORGSTATUS_ACTIVE
+    ORGANIZATION_STATUSES, ORGSTATUS_ACTIVE,\
+    ROLE_KINDS
 from .managers import OrganizationQuerySet
 
+from scipost.constants import TITLE_CHOICES
+from scipost.fields import ChoiceArrayField
 from scipost.models import Contributor
 from journals.models import Publication, OrgPubFraction, UnregisteredAuthor
 
@@ -175,3 +182,71 @@ class Organization(models.Model):
         for agreement in self.partner.agreements.all():
             contrib += agreement.offered_yearly_contribution * int(agreement.duration.days / 365)
         return contrib
+
+
+
+####################################
+# Contact persons, users and roles #
+####################################
+
+class ContactPerson(models.Model):
+    """
+    A ContactPerson instance holds information about a person who can function
+    as a contact for one or more organizations.
+    These instances are created by SPAdmin during sponsor harvesting.
+    Instances can be promoted to Contact instances, which possess login credentials.
+    """
+    organization = models.ForeignKey('organizations.Organization',
+                                     on_delete=models.CASCADE)
+    title = models.CharField(max_length=4, choices=TITLE_CHOICES)
+    first_name = models.CharField(max_length=64)
+    last_name = models.CharField(max_length=64)
+    email = models.EmailField()
+    role = models.CharField(max_length=128)
+
+    def __str__(self):
+        return "%s %s %s" % (self.get_title_display(), self.first_name, self.last_name)
+
+
+class Contact(models.Model):
+    """
+    A Contact instance is a basic User to be used for Organization-type contacts.
+    Specific Organizations are linked to Contact via the ContactRole model defined below.
+    """
+    user = models.OneToOneField(User, on_delete=models.CASCADE, unique=True,
+                                related_name='org_contact')
+    title = models.CharField(max_length=4, choices=TITLE_CHOICES)
+    activation_key = models.CharField(max_length=40, blank=True)
+    key_expires = models.DateTimeField(default=timezone.now)
+
+    def __str__(self):
+        return '%s %s, %s' % (self.get_title_display(), self.user.last_name, self.user.first_name)
+
+    def generate_key(self, feed=''):
+        """
+        Generate and save a new activation_key for the Contact, given a certain feed.
+        """
+        for i in range(5):
+            feed += random.choice(string.ascii_letters)
+        feed = feed.encode('utf8')
+        salt = self.user.username.encode('utf8')
+        self.activation_key = hashlib.sha1(salt + feed).hexdigest()
+        self.key_expires = timezone.now() + datetime.timedelta(days=2)
+
+    def save(self, *args, **kwargs):
+        if not self.activation_key:
+            self.generate_key()
+        super().save(*args, **kwargs)
+
+
+class ContactRole(models.Model):
+    """
+    A ContactRole instance links a Contact to an Organization, for a specific period of
+    time, and for a specific period in time.
+    """
+    contact = models.ForeignKey('organizations.Contact', on_delete=models.CASCADE,
+                                related_name='roles')
+    organization = models.ForeignKey('organizations.Organization', on_delete=models.CASCADE)
+    kind = ChoiceArrayField(models.CharField(max_length=4, choices=ROLE_KINDS))
+    date_from = models.DateField()
+    date_until = models.DateField()
diff --git a/partners/models.py b/partners/models.py
index fbcd487e7..19ee9d0d6 100644
--- a/partners/models.py
+++ b/partners/models.py
@@ -82,6 +82,7 @@ class ProspectivePartner(models.Model):
         self.save()
 
 
+# TODO: to be deleted, superseded by organizations.ContactPerson
 class ProspectiveContact(models.Model):
     """
     A ProspectiveContact is a person's name and contact details, with a
-- 
GitLab