diff --git a/organizations/constants.py b/organizations/constants.py index 6a6056ac8557cae158ec3e826c3854e3ca011110..ba57d4e4412b2f6a39cc5edb52e35d091bbbb04a 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 0000000000000000000000000000000000000000..08ae732bb27575d7233d031f6c340518a163ed0d --- /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 e2aded3dbe630b025a2c898c79cb2ba35b2d1272..fc5b30655b19fd9ffb38a9fd0f18b4f40cdf62d3 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 fbcd487e7c2718392010ab830426c792a09aa6eb..19ee9d0d69e44ca7de17b645e20af6e01dc1931e 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