margin: .5em 0 .5em .75em;
}
-h2 a, h3 a {
+h2 a, h3 a, h4 a {
text-decoration: none;
color: #515151;
}
from models import PendingAdditionalOrder
from models import VolunteerSlot
from models import AccessToken
+from models import ConferenceNews
from selectable.forms.widgets import AutoCompleteSelectWidget, AutoCompleteSelectMultipleWidget
from postgresqleu.accountinfo.lookups import UserLookup
admin.site.register(PendingAdditionalOrder, PendingAdditionalOrderAdmin)
admin.site.register(VolunteerSlot, VolunteerSlotAdmin)
admin.site.register(AccessToken)
+admin.site.register(ConferenceNews)
from postgresqleu.confreg.models import ConferenceSessionScheduleSlot, VolunteerSlot
from postgresqleu.confreg.models import DiscountCode, AccessToken, AccessTokenPermissions
from postgresqleu.confreg.models import ConferenceSeries
+from postgresqleu.confreg.models import ConferenceNews
+from postgresqleu.newsevents.models import NewsPosterProfile
from postgresqleu.confreg.models import valid_status_transitions, get_status_string
'token': generate_random_token()
}
+
+class BackendNewsForm(BackendForm):
+ helplink = 'news'
+ list_fields = ['title', 'datetime', 'author' ]
+ markdown_fields = ['summary', ]
+ exclude_date_validators = ['datetime', ]
+ defaultsort = [[1, "desc"]]
+
+ class Meta:
+ model = ConferenceNews
+ fields = ['author', 'datetime', 'title', 'inrss', 'summary' ]
+
+ def fix_fields(self):
+ # Must be administrator on current conference
+ self.fields['author'].queryset = NewsPosterProfile.objects.filter(author__conference=self.conference)
+ # Add help hint dynamically so we can include the conference name
+ self.fields['title'].help_text = 'Note! Title wil be prefixed with "{0} - " on shared frontpage and RSS!'.format(self.conference.conferencename)
+
+
#
# Form to pick a conference to copy from
#
from backendforms import BackendFeedbackQuestionForm, BackendDiscountCodeForm
from backendforms import BackendAccessTokenForm
from backendforms import BackendConferenceSeriesForm
+from backendforms import BackendNewsForm
def get_authenticated_conference(request, urlname):
if not request.user.is_authenticated:
BackendAccessTokenForm,
rest)
+def edit_news(request, urlname, rest):
+ return backend_list_editor(request,
+ urlname,
+ BackendNewsForm,
+ rest)
+
###
# Non-simple-editor views
from django.contrib.syndication.views import Feed
from django.conf import settings
+from django.shortcuts import get_object_or_404
from models import Conference
import datetime
+from postgresqleu.util.db import exec_to_dict
+
class LatestEvents(Feed):
title = "Events - %s" % settings.ORG_NAME
link = "/"
def item_link(self, conference):
return "%s/events/%s/" % (settings.SITEBASE, conference.urlname)
+
+class ConferenceNewsFeed(Feed):
+ description_template = "pieces/news_description.html"
+
+ def get_object(self, request, what):
+ return get_object_or_404(Conference, urlname=what)
+
+ def title(self, obj):
+ return "News - {0}".format(obj.conferencename)
+
+ def description(self, obj):
+ return "Latest news from {0}".format(obj.conferencename)
+
+ def link(self, obj):
+ return obj.confurl
+
+ def items(self, obj):
+ return exec_to_dict("""SELECT n.id, c.confurl AS link, datetime, c.conferencename || ' - ' || title AS title, summary
+FROM confreg_conferencenews n
+INNER JOIN confreg_conference c ON c.id=conference_id
+WHERE datetime<CURRENT_TIMESTAMP AND inrss AND conference_id=%(cid)s
+ORDER BY datetime DESC LIMIT 10""", {
+ 'cid': obj.id,
+ })
+
+ def item_title(self, news):
+ return news['title']
+
+ def item_link(self, news):
+ return '{0}##{1}'.format(news['link'], news['id'])
--- /dev/null
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.10 on 2018-09-21 16:35
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+import datetime
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('newsevents', '0003_conferencenews'),
+ ('confreg', '0027_confpromo'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ConferenceNews',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('datetime', models.DateTimeField(default=datetime.datetime.now)),
+ ('title', models.CharField(max_length=128)),
+ ('summary', models.TextField()),
+ ('inrss', models.BooleanField(default=True, verbose_name=b'Include in RSS feed')),
+ ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='newsevents.NewsPosterProfile')),
+ ('conference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='confreg.Conference')),
+ ],
+ options={
+ 'ordering': ['-datetime'],
+ 'verbose_name_plural': 'Conference News',
+ },
+ ),
+ ]
from postgresqleu.countries.models import Country
from postgresqleu.invoices.models import Invoice, VatRate
+from postgresqleu.newsevents.models import NewsPosterProfile
+
from regtypes import special_reg_types
SKILL_CHOICES = (
def _display_permissions(self):
return ", ".join(self.permissions)
+
+
+
+class ConferenceNews(models.Model):
+ conference = models.ForeignKey(Conference, null=False, on_delete=models.CASCADE)
+ datetime = models.DateTimeField(blank=False, default=datetime.datetime.now)
+ title = models.CharField(max_length=128, blank=False)
+ summary = models.TextField(blank=False)
+ author = models.ForeignKey(NewsPosterProfile)
+ inrss = models.BooleanField(null=False, default=True, verbose_name="Include in RSS feed")
+
+ def __unicode__(self):
+ return self.title
+
+ class Meta:
+ ordering = ['-datetime', ]
+ verbose_name_plural = 'Conference News'
from models import PendingAdditionalOrder
from models import RegistrationWaitlistEntry, RegistrationWaitlistHistory
from models import STATUS_CHOICES
+from models import ConferenceNews
from forms import ConferenceRegistrationForm, RegistrationChangeForm, ConferenceSessionFeedbackForm
from forms import ConferenceFeedbackForm, SpeakerProfileForm
from forms import CallForPapersForm, CallForPapersSpeakerForm
from StringIO import StringIO
import json
+import markdown
#
# Render a conference page. It will load the template using the jinja system
return HttpResponseRedirect(conference.confurl)
+def news_json(request, confname):
+ news = ConferenceNews.objects.select_related('author').filter(conference__urlname=confname,
+ inrss=True,
+ datetime__lt=datetime.now(),
+ )[:5]
+
+ r = HttpResponse(json.dumps(
+ [{
+ 'title': n.title,
+ 'datetime': n.datetime,
+ 'authorname': n.author.fullname,
+ 'summary': markdown.markdown(n.summary),
+ } for n in news],
+ cls=JsonSerializer), content_type='application/json')
+
+ r['Access-Control-Allow-Origin'] = '*'
+ return r
+
+
@login_required
@transaction.atomic
def register(request, confname, whatfor=None):
from django.contrib import admin
-from postgresqleu.newsevents.models import News
+
+from selectable.forms.widgets import AutoCompleteSelectWidget
+
+from postgresqleu.accountinfo.lookups import UserLookup
+from postgresqleu.util.forms import ConcurrentProtectedModelForm
+from postgresqleu.util.admin import SelectableWidgetAdminFormMixin
+
+from postgresqleu.newsevents.models import News, NewsPosterProfile
+
+class NewsPosterProfileForm(SelectableWidgetAdminFormMixin, ConcurrentProtectedModelForm):
+ class Meta:
+ model = NewsPosterProfile
+ exclude = []
+ widgets = {
+ 'author': AutoCompleteSelectWidget(lookup_class=UserLookup),
+ }
+
+class NewsPosterProfileAdmin(admin.ModelAdmin):
+ form = NewsPosterProfileForm
+ list_display = ('__unicode__', 'rsslink')
+
+ def rsslink(self, author):
+ return "/feeds/user/{0}/".format(author.urlname)
admin.site.register(News)
+admin.site.register(NewsPosterProfile, NewsPosterProfileAdmin)
from django.contrib.syndication.views import Feed
+from django.http import Http404
+from django.template.defaultfilters import slugify
+from django.shortcuts import get_object_or_404
from django.conf import settings
-from models import News
+from models import News, NewsPosterProfile
import datetime
+from postgresqleu.util.db import exec_to_dict
+
class LatestNews(Feed):
title = "News - %s" % settings.ORG_NAME
link = "/"
- description = "The latest news from %s" % settings.ORG_NAME
description_template = "pieces/news_description.html"
-
- def items(self):
- return News.objects.all()[:10]
-
+
+ def get_object(self, request, what):
+ if what == 'news':
+ return None
+ elif what.startswith('user/'):
+ a = get_object_or_404(NewsPosterProfile, urlname=what.split('/')[1])
+ self.item_author_name = a.fullname
+ return a
+ raise Http404("Feed not found")
+
+ def description(self, obj):
+ if obj:
+ return "The latest news from {0} by {1}".format(settings.ORG_NAME, obj.fullname)
+ else:
+ return "The latest news from {0}".format(settings.ORG_NAME)
+
+ def items(self, obj):
+ # Front page news is a mix of global and conference news, possibly
+ # filtered by NewsPosterProfile.
+ if obj == None:
+ extrafilter = ""
+ params = {}
+ else:
+ extrafilter = " AND author_id=%(authorid)s"
+ params = {
+ 'authorid': obj.pk,
+ }
+
+ return exec_to_dict("""WITH main AS (
+ SELECT id, NULL::text as link, datetime, title, summary
+ FROM newsevents_news
+ WHERE datetime<CURRENT_TIMESTAMP AND inrss {0}
+ ORDER BY datetime DESC LIMIT 10),
+conf AS (
+ SELECT n.id, c.confurl AS link, datetime, c.conferencename || ' - ' || title AS title, summary
+ FROM confreg_conferencenews n
+ INNER JOIN confreg_conference c ON c.id=conference_id
+ WHERE datetime<CURRENT_TIMESTAMP AND inrss {0}
+ ORDER BY datetime DESC LIMIT 10)
+SELECT id, link, datetime, title, summary, true AS readmore FROM main
+UNION ALL
+SELECT id, link, datetime, title, summary, false FROM conf
+ORDER BY datetime DESC LIMIT 10""".format(extrafilter), params)
+
+ def item_title(self, news):
+ return news['title']
+
def item_link(self, news):
- return "%s/news/%s" % (settings.SITEBASE, news.id)
+ if news['link']:
+ return '{0}##{1}'.format(news['link'], news['id'])
+ else:
+ return '{0}/news/{1}-{2}/'.format(
+ settings.SITEBASE,
+ slugify(news['title']),
+ news['id'],
+ )
--- /dev/null
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.10 on 2018-09-21 16:35
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('auth', '0008_alter_user_username_max_length'),
+ ('newsevents', '0002_drop_standalone_events'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='NewsPosterProfile',
+ fields=[
+ ('author', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
+ ('urlname', models.CharField(max_length=50, unique=True)),
+ ('fullname', models.CharField(max_length=100)),
+ ('canpostglobal', models.BooleanField(default=False)),
+ ],
+ ),
+ migrations.AddField(
+ model_name='news',
+ name='highpriorityuntil',
+ field=models.DateTimeField(null=True, blank=True, verbose_name=b'High priority until'),
+ ),
+ migrations.AddField(
+ model_name='news',
+ name='inarchive',
+ field=models.BooleanField(default=True, verbose_name=b'Include in archives'),
+ ),
+ migrations.AddField(
+ model_name='news',
+ name='inrss',
+ field=models.BooleanField(default=True, verbose_name=b'Include in RSS feed'),
+ ),
+ migrations.AddField(
+ model_name='news',
+ name='author',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='newsevents.NewsPosterProfile'),
+ ),
+ ]
from django.db import models
-from postgresqleu.countries.models import Country
+from django.contrib.auth.models import User
+
+class NewsPosterProfile(models.Model):
+ author = models.OneToOneField(User, primary_key=True)
+ urlname = models.CharField(max_length=50, null=False, blank=False, unique=True)
+ fullname = models.CharField(max_length=100, null=False, blank=False)
+ canpostglobal = models.BooleanField(null=False, default=False)
+
+ def __unicode__(self):
+ return u"{0} ({1})".format(self.fullname, self.urlname)
+
class News(models.Model):
title = models.CharField(max_length=128, blank=False)
datetime = models.DateTimeField(blank=False)
summary = models.TextField(blank=False)
-
- def __str__(self):
+ author = models.ForeignKey(NewsPosterProfile, null=True)
+ highpriorityuntil = models.DateTimeField(null=True, blank=True, verbose_name="High priority until")
+ inrss = models.BooleanField(null=False, default=True, verbose_name="Include in RSS feed")
+ inarchive = models.BooleanField(null=False, default=True, verbose_name="Include in archives")
+
+ def __unicode__(self):
return self.title
@property
from django.shortcuts import render, get_object_or_404
+from django.core import paginator
import datetime
from postgresqleu.newsevents.models import News
def newsitem(request, itemid):
- item = get_object_or_404(News, pk=itemid, datetime__lte=datetime.datetime.today())
- news = News.objects.filter(datetime__lte=datetime.datetime.today())[:5]
+ item = get_object_or_404(News.objects.select_related('author'),
+ pk=itemid, datetime__lte=datetime.datetime.today())
+ news = News.objects.filter(datetime__lte=datetime.datetime.today(), inarchive=True)[:10]
return render(request, 'newsevents/news.html', {
'item': item,
'news': news,
})
+
+def newsarchive(request):
+ news = News.objects.filter(datetime__lte=datetime.datetime.today(), inarchive=True)[:10]
+ allnews = News.objects.filter(datetime__lte=datetime.datetime.today(), inarchive=True)
+
+ p = paginator.Paginator(allnews, 15)
+
+ page = request.GET.get('page', 1)
+ try:
+ newspage = p.page(page)
+ except paginator.PageNotAnInteger:
+ newspage = p.page(1)
+ except paginator.EmptyPage:
+ newspage = p.page(p.num_pages)
+
+ return render(request, 'newsevents/archive.html', {
+ 'news': news,
+ 'newspage': newspage,
+ })
import postgresqleu.accountinfo.views
from postgresqleu.newsevents.feeds import LatestNews
-from postgresqleu.confreg.feeds import LatestEvents
+from postgresqleu.confreg.feeds import LatestEvents, ConferenceNewsFeed
# Uncomment the next two lines to enable the admin:
# from django.contrib import admin
url(r'^events/past/$', postgresqleu.views.pastevents),
url(r'^(events/services)/$', postgresqleu.static.views.static_fallback),
url(r'^events/series/[^/]+-(\d+)/$', postgresqleu.views.eventseries),
+ url(r'^news/archive/$', postgresqleu.newsevents.views.newsarchive),
url(r'news/[^/]+-(\d+)/$', postgresqleu.newsevents.views.newsitem),
# Log in/log out
url(r'^auth_receive/$', postgresqleu.auth.auth_receive),
# Feeds
- url(r'^feeds/news/$', LatestNews()),
+ url(r'^feeds/(?P<what>(news|user/[^/]+))/$', LatestNews()),
+ url(r'^feeds/conf/(?P<what>[^/]+)/$', ConferenceNewsFeed()),
+ url(r'^feeds/conf/(?P<confname>[^/]+)/json/$', postgresqleu.confreg.views.news_json),
# Conference management
url(r'^events/(?P<confname>[^/]+)/register/(?P<whatfor>(self)/)?$', postgresqleu.confreg.views.register),
url(r'^events/admin/(\w+)/feedbackquestions/(.*/)?$', postgresqleu.confreg.backendviews.edit_feedbackquestions),
url(r'^events/admin/(\w+)/discountcodes/(.*/)?$', postgresqleu.confreg.backendviews.edit_discountcodes),
url(r'^events/admin/(\w+)/accesstokens/(.*/)?$', postgresqleu.confreg.backendviews.edit_accesstokens),
+ url(r'^events/admin/(\w+)/news/(.*/)?$', postgresqleu.confreg.backendviews.edit_news),
url(r'^events/admin/(\w+)/pendinginvoices/$', postgresqleu.confreg.backendviews.pendinginvoices),
url(r'^events/admin/(\w+)/purgedata/$', postgresqleu.confreg.backendviews.purge_personal_data),
url(r'^events/admin/([^/]+)/talkvote/$', postgresqleu.confreg.views.talkvote),
# Index has a very special view that lives out here
from django.shortcuts import render, get_object_or_404
+from django.template.defaultfilters import slugify
from postgresqleu.newsevents.models import News
from postgresqleu.confreg.models import Conference, ConferenceSeries
+from postgresqleu.util.db import exec_to_dict
+
import datetime
+import markdown
# Handle the frontpage
def index(request):
where=["EXISTS (SELECT 1 FROM confreg_conference c WHERE c.series_id=confreg_conferenceseries.id AND c.promoactive)"]
)
- news = News.objects.filter(datetime__lte=datetime.datetime.today())[:5]
+ # Native query, because django ORM vs UNION...
+ # If a news item has the flag "high priority until" until a date that's still in the future,
+ # make sure it always bubbles to the top of the list. We do this by creating a secondary ordering
+ # field to order by first. To make sure we capture all such things, we need to get at least the
+ # same number of items from each subset and then LIMIT it once again for the total limit.
+ news = exec_to_dict("""WITH main AS (
+ SELECT id, NULL::text AS confurl, CASE WHEN highpriorityuntil > CURRENT_TIMESTAMP THEN 1 ELSE 0 END AS priosort, datetime, title, summary
+ FROM newsevents_news
+ WHERE datetime<CURRENT_TIMESTAMP ORDER BY datetime DESC LIMIT 5),
+conf AS (
+ SELECT n.id, c.confurl, 0 AS priosort, datetime, c.conferencename || ': ' || title AS title, summary
+ FROM confreg_conferencenews n
+ INNER JOIN confreg_conference c ON c.id=conference_id
+ WHERE datetime<CURRENT_TIMESTAMP
+ ORDER BY datetime DESC LIMIT 5)
+SELECT id, confurl, datetime, title, summary, priosort FROM main
+UNION ALL
+SELECT id, confurl, datetime, title, summary, priosort FROM conf
+ORDER BY priosort DESC, datetime DESC LIMIT 5""")
+ for n in news:
+ n['summaryhtml'] = markdown.markdown(n['summary'])
+ if n['confurl']:
+ n['itemlink'] = n['confurl']
+ else:
+ n['itemlink'] = '/news/{0}-{1}/'.format(slugify(n['title']), n['id'])
+
return render(request, 'index.html', {
'events': events,
'series': series,
<div class="col-md-3 col-sm-6 col-xs-12 buttonrow"><a class="btn btn-default btn-block" href="/events/sponsor/admin/{{c.urlname}}/sendmail/">Sponsor emails</a></div>
</div>
+<h2>News</h2>
+<div class="row">
+ <div class="col-md-3 col-sm-6 col-xs-12 buttonrow"><a class="btn btn-default btn-block" href="/events/admin/{{c.urlname}}/news/">News</a></div>
+</div>
<h2>Reports</h2>
<div class="row">
<h2 class="text-center">Latest news <a href="/feeds/news/"><i class="fa fa-rss"></i></a></h2>
{%for n in news%}
<div class="{%if forloop.counter > 2%}d-none d-lg-block{%endif%}">
- <h4 class="text-center">{{n.title}}</h4>
+ <h4 class="text-center"><a href="{{n.itemlink}}">{{n.title}}</a></h4>
<div class="text-center small"><i class="fa fa-clock-o"></i> {{n.datetime|date:"Y-m-d"}}</div>
<div class="newscontent">
- {{n.summary|markdown|truncatewords_html:100}}
+ {{n.summaryhtml|safe|truncatewords_html:100}}
</div>
- <a href="/news/{{n.title|slugify}}-{{n.id}}/" class="btn btn-primary btn-sm">Read more</a>
+{%if n.summaryhtml|truncatewords_html:100 != n.summaryhtml%}
+ <a href="{{n.itemlink}}" class="btn btn-primary btn-sm">Read more</a>
+{%endif%}
</div>
{%endfor%}
</div>
--- /dev/null
+{%extends "navbase.html"%}
+{%load markup%}
+{%block title%}News Archive{%endblock%}
+{%block navsection%}News{%endblock%}
+{%block content%}
+<h1>News archive</h1>
+
+{%for n in newspage%}
+<h3>{{n.title}}</h3>
+<i class="fa fa-clock-o"></i> {{n.datetime|date:"Y-m-d"}}
+<div class="newscontent">
+{{n.summary|markdown|truncatewords_html:75}}
+</div>
+<a href="/news/{{n.title|slugify}}-{{n.id}}/" class="btn btn-primary btn-sm">Read more</a>
+{%endfor%}
+
+<br/><br/>
+
+<nav aria-label="News pages">
+<ul class="pagination pagination-sm">
+ <li class="page-item{%if not newspage.has_previous%} disabled{%endif%}"><a class="page-link" href="?page={%if newspage.has_previous%}{{newspage.previous_page_number}}{%else%}1{%endif%}">«</a></li>
+{%for p in newspage.paginator.page_range %}
+ <li class="page-item{%if newspage.number == p%} active{%endif%}"><a class="page-link" href="?page={{p}}">{{p}}</a></li>
+{%endfor%}
+ <li class="page-item{%if not newspage.has_next%} disabled{%endif%}"><a class="page-link" href="?page={%if newspage.has_next%}{{newspage.next_page_number}}{%else%}{{0}}{%endif%}">»</a></li>
+</ul>
+</nav>
+
+{%endblock%}
+
+{%block navblock%}
+{%for n in news %}
+ <li><a href="/news/{{n.title|slugify}}-{{n.id}}/">{{n.title}}</a></li>
+{%endfor%}
+<br/>
+<a href="/news/archive/" class="btn btn-primary btn-sm">Older news</a>
+{%endblock%}
{%block navsection%}News{%endblock%}
{%block content%}
<h1>{{item.title}}</h1>
-<i class="fa fa-clock-o"></i> {{item.datetime|date:"Y-m-d"}}
+<i class="fa fa-clock-o"></i> {{item.datetime|date:"Y-m-d"}}{%if item.author%} by {{item.author.fullname}}{%endif%}
<p></p>
{{item.summary|markdown}}
{%endblock%}
{%for n in news %}
<li><a href="/news/{{n.title|slugify}}-{{n.id}}/">{{n.title}}</a></li>
{%endfor%}
+<br/>
+<a href="/news/archive/" class="btn btn-primary btn-sm">Older news</a>
{%endblock%}