# Submitted items
url(r'^(?P<objtype>news)/(?P<item>\d+)/(?P<what>submit|withdraw)/$', pgweb.account.views.submitted_item_submitwithdraw),
url(r'^(?P<objtype>news|events|products|organisations|services)/(?P<item>\d+|new)/$', pgweb.account.views.submitted_item_form),
+ url(r'^organisations/confirm/([0-9a-f]+)/$', pgweb.account.views.confirm_org_email),
# Organisation information
url(r'^orglist/$', pgweb.account.views.orglist),
from pgweb.news.models import NewsArticle
from pgweb.events.models import Event
from pgweb.core.models import Organisation, UserProfile, ModerationNotification
+from pgweb.core.models import OrganisationEmail
from pgweb.contributors.models import Contributor
from pgweb.downloads.models import Product
from pgweb.profserv.models import ProfessionalService
extracontext=extracontext)
+@login_required
+@transaction.atomic
+def confirm_org_email(request, token):
+ try:
+ email = OrganisationEmail.objects.get(token=token)
+ except OrganisationEmail.DoesNotExist:
+ raise Http404()
+
+ if not email.org.managers.filter(pk=request.user.pk).exists():
+ raise PermissionDenied("You are not a manager of the associated organisation")
+
+ email.confirmed = True
+ email.token = None
+ email.save()
+
+ return HttpResponseRedirect('/account/organisations/{}/'.format(email.org.id))
+
+
@content_sources('style', "'unsafe-inline'")
def _submitted_item_submit(request, objtype, model, obj):
if obj.modstate != ModerationState.CREATED:
from django.forms import ValidationError
from django.conf import settings
-from .models import Organisation
+from .models import Organisation, OrganisationEmail
from django.contrib.auth.models import User
from pgweb.util.middleware import get_current_user
from pgweb.util.moderation import ModerationState
from pgweb.mailqueue.util import send_simple_mail
+from pgweb.util.misc import send_template_mail, generate_random_token
class OrganisationForm(forms.ModelForm):
+ remove_email = forms.ModelMultipleChoiceField(required=False, queryset=None, label="Current email addresses", help_text="Select one or more email addresses to remove")
+ add_email = forms.EmailField(required=False, help_text="Enter an email address to add")
remove_manager = forms.ModelMultipleChoiceField(required=False, queryset=None, label="Current manager(s)", help_text="Select one or more managers to remove")
add_manager = forms.EmailField(required=False)
del self.fields['remove_manager']
del self.fields['add_manager']
+ if self.instance and self.instance.pk and self.instance.is_approved:
+ # Only allow adding/removing emails on orgs that are actually approved
+ self.fields['remove_email'].queryset = OrganisationEmail.objects.filter(org=self.instance)
+ else:
+ del self.fields['remove_email']
+ del self.fields['add_email']
+
+ def clean_add_email(self):
+ if self.cleaned_data['add_email']:
+ if OrganisationEmail.objects.filter(org=self.instance, address=self.cleaned_data['add_email'].lower()).exists():
+ raise ValidationError("This email is already registered for your organisation.")
+ return self.cleaned_data['add_email']
+
def clean_add_manager(self):
if self.cleaned_data['add_manager']:
# Something was added as manager - let's make sure the user exists
def save(self, commit=True):
model = super(OrganisationForm, self).save(commit=False)
+
ops = []
+ if self.cleaned_data.get('add_email', None):
+ # Create the email record
+ e = OrganisationEmail(org=model, address=self.cleaned_data['add_email'].lower(), token=generate_random_token())
+ e.save()
+
+ # Send email for confirmation
+ send_template_mail(
+ settings.NOTIFICATION_FROM,
+ e.address,
+ "Email address added to postgresql.org organisation",
+ 'core/org_add_email.txt',
+ {
+ 'org': model,
+ 'email': e,
+ },
+ )
+ ops.append('Added email {}, confirmation request sent'.format(e.address))
+ if self.cleaned_data.get('remove_email', None):
+ for e in self.cleaned_data['remove_email']:
+ ops.append('Removed email {}'.format(e.email))
+ e.delete()
+
if 'add_manager' in self.cleaned_data and self.cleaned_data['add_manager']:
u = User.objects.get(email=self.cleaned_data['add_manager'].lower())
model.managers.add(u)
send_simple_mail(
settings.NOTIFICATION_FROM,
settings.NOTIFICATION_EMAIL,
- "{0} modified managers of {1}".format(get_current_user().username, model),
- "The following changes were made to managers:\n\n{0}".format("\n".join(ops))
+ "{0} modified {1}".format(get_current_user().username, model),
+ "The following changes were made to {}:\n\n{}".format(model, "\n".join(ops))
)
return model
from datetime import datetime, timedelta
from pgweb.account.models import SecondaryEmail
+from pgweb.core.models import OrganisationEmail
class Command(BaseCommand):
# Clean up old email change tokens
with transaction.atomic():
SecondaryEmail.objects.filter(confirmed=False, sentat__lt=datetime.now() - timedelta(hours=24)).delete()
+ OrganisationEmail.objects.filter(confirmed=False, added__lt=datetime.now() - timedelta(hours=72)).delete()
--- /dev/null
+# Generated by Django 2.2.11 on 2020-09-07 12:53
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0003_mailtemplate'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='organisation',
+ name='email',
+ ),
+ migrations.RemoveField(
+ model_name='organisation',
+ name='phone',
+ ),
+ migrations.CreateModel(
+ name='OrganisationEmail',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('address', models.EmailField(max_length=254)),
+ ('confirmed', models.BooleanField(default=False)),
+ ('token', models.CharField(blank=True, max_length=100, null=True)),
+ ('org', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Organisation')),
+ ('added', models.DateTimeField(null=False, auto_now_add=True)),
+ ],
+ options={
+ 'ordering': ('org', 'address'),
+ 'unique_together': {('org', 'address')},
+ },
+ ),
+ ]
name = models.CharField(max_length=100, null=False, blank=False, unique=True)
address = models.TextField(null=False, blank=True)
url = models.URLField(null=False, blank=False)
- email = models.EmailField(null=False, blank=True)
- phone = models.CharField(max_length=100, null=False, blank=True)
orgtype = models.ForeignKey(OrganisationType, null=False, blank=False, verbose_name="Organisation type", on_delete=models.CASCADE)
managers = models.ManyToManyField(User, blank=False)
mailtemplate = models.CharField(max_length=50, null=False, blank=False, default='default', choices=_mail_template_choices,
lastconfirmed = models.DateTimeField(null=False, blank=False, auto_now_add=True)
account_edit_suburl = 'organisations'
- moderation_fields = ['address', 'url', 'email', 'phone', 'orgtype', 'managers']
+ moderation_fields = ['address', 'url', 'orgtype', 'managers']
def __str__(self):
return self.name
return OrganisationForm
+class OrganisationEmail(models.Model):
+ org = models.ForeignKey(Organisation, null=False, blank=False, on_delete=models.CASCADE)
+ address = models.EmailField(null=False, blank=False)
+ confirmed = models.BooleanField(null=False, blank=False, default=False)
+ token = models.CharField(max_length=100, null=True, blank=True)
+ added = models.DateTimeField(null=False, blank=False, auto_now_add=True)
+
+ class Meta:
+ ordering = ('org', 'address')
+ unique_together = (
+ ('org', 'address', ),
+ )
+
+ def __str__(self):
+ if self.confirmed:
+ return self.address
+ return "{} (not confirmed yet)".format(self.address)
+
+
# Basic classes for importing external RSS feeds, such as planet
class ImportedRSSFeed(models.Model):
internalname = models.CharField(max_length=32, null=False, blank=False, unique=True)
from django.contrib import admin
+from django import forms
from pgweb.util.admin import PgwebAdmin
+from pgweb.core.models import OrganisationEmail
from .models import NewsArticle, NewsTag
+class NewsArticleAdminForm(forms.ModelForm):
+ model = NewsArticle
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ if self.instance:
+ self.fields['email'].queryset = OrganisationEmail.objects.filter(org=self.instance.org, confirmed=True)
+
+
class NewsArticleAdmin(PgwebAdmin):
list_display = ('title', 'org', 'date', 'modstate', )
list_filter = ('modstate', )
filter_horizontal = ('tags', )
search_fields = ('content', 'title', )
exclude = ('modstate', 'firstmoderator', )
+ form = NewsArticleAdminForm
class NewsTagAdmin(PgwebAdmin):
from django.forms import ValidationError
from pgweb.util.moderation import ModerationState
-from pgweb.core.models import Organisation
+from pgweb.core.models import Organisation, OrganisationEmail
from .models import NewsArticle, NewsTag
class NewsArticleForm(forms.ModelForm):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields['email'].required = True
+
def filter_by_user(self, user):
self.fields['org'].queryset = Organisation.objects.filter(managers=user, approved=True)
+ self.fields['email'].queryset = OrganisationEmail.objects.filter(org__managers=user, org__approved=True, confirmed=True)
def clean_date(self):
if self.instance.pk and self.instance.modstate != ModerationState.CREATED:
def clean(self):
data = super().clean()
+ if data.get('email', None):
+ if data['email'].org != data['org']:
+ self.add_error('email', 'You must pick an email address associated with the organisation')
+
if 'tags' not in data:
self.add_error('tags', 'Select one or more tags')
else:
--- /dev/null
+# Generated by Django 2.2.11 on 2020-09-07 14:13
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0004_org_emails'),
+ ('news', '0005_modstate'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='newsarticle',
+ name='email',
+ field=models.ForeignKey(blank=True, help_text='Pick a confirmed email associated with the organisation. This will be used as the reply address of posted news.', null=True, on_delete=django.db.models.deletion.CASCADE, to='core.OrganisationEmail', verbose_name='Reply email'),
+ ),
+ ]
from django.db import models
from datetime import date
-from pgweb.core.models import Organisation
+from pgweb.core.models import Organisation, OrganisationEmail
from pgweb.util.moderation import TristateModerateModel, ModerationState, TwoModeratorsMixin
from .util import send_news_email, render_news_template, embed_images_in_html
class NewsArticle(TwoModeratorsMixin, TristateModerateModel):
org = models.ForeignKey(Organisation, null=False, blank=False, verbose_name="Organisation", help_text="If no organisations are listed, please check the <a href=\"/account/orglist/\">organisation list</a> and contact the organisation manager or <a href=\"mailto:webmaster@postgresql.org\">webmaster@postgresql.org</a> if none are listed.", on_delete=models.CASCADE)
+ email = models.ForeignKey(OrganisationEmail, null=True, blank=True, verbose_name="Reply email", help_text="Pick a confirmed email associated with the organisation. This will be used as the reply address of posted news.", on_delete=models.CASCADE)
date = models.DateField(null=False, blank=False, default=date.today)
title = models.CharField(max_length=200, null=False, blank=False)
content = models.TextField(null=False, blank=False)
account_edit_suburl = 'news'
markdown_fields = ('content',)
- moderation_fields = ('org', 'sentfrom', 'replyto', 'date', 'title', 'content', 'taglist')
- preview_fields = ('title', 'sentfrom', 'replyto', 'content', 'taglist')
+ moderation_fields = ('org', 'sentfrom', 'email', 'date', 'title', 'content', 'taglist')
+ preview_fields = ('title', 'sentfrom', 'email', 'content', 'taglist')
+ notify_fields = ('org', 'email', 'date', 'title', 'content', 'tags')
rendered_preview_fields = ('content', )
extramodnotice = "In particular, note that news articles will be sent by email to subscribers, and therefor cannot be recalled in any way once sent."
def taglist(self):
return ", ".join([t.name for t in self.tags.all()])
- @property
- def replyto(self):
- return self.org.email
-
@property
def sentfrom(self):
return self.org.fromnameoverride if self.org.fromnameoverride else '{} via PostgreSQL Announce'.format(self.org.name)
return 'Title/subject'
elif f == 'sentfrom':
return 'Sent from'
- elif f == 'replyto':
+ elif f == 'email':
return 'Direct replies to'
elif f == 'taglist':
return 'List of tags'
settings.NEWS_MAIL_RECEIVER,
news.title,
news.content,
- replyto=news.replyto,
+ replyto=news.email.address,
sendername=news.sentfrom,
receivername=settings.NEWS_MAIL_RECEIVER_NAME,
messageid=messageid,
--- /dev/null
+Hello!
+
+You are receiving this email because this address was added
+to the organisation {{org}}
+on www.postgresql.org.
+
+If this is not correct, please ignore this email and the
+record of the address will be automatically deleted.
+
+If this is correct, please click the below link to confirm
+this email address.
+
+{{link_root}}/account/organisations/confirm/{{email.token}}/