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