Revamp and expand news handling
authorMagnus Hagander <[email protected]>
Sun, 23 Sep 2018 12:45:09 +0000 (14:45 +0200)
committerMagnus Hagander <[email protected]>
Sun, 23 Sep 2018 12:58:22 +0000 (14:58 +0200)
A number of improvements and unifications for news:
* News posts now get authors

* Authors are from NewsPosterProfile:s, which will include a full name
  and an "urlname"

* Authors can also be specified as "can post global", which should
  give rights to post on the global feed. Right now the global feed
  requires superuser access anwyay, but for the future...

* News can now be posted at conference level as well as previous
  global only

* Front page of website pulls in a combination of global news and
  conference news. Global news can be given a "high priority until"
  field that ensure it sits at the top of the frontpage until a
  certain date, so conference news can't push it off.

* Each conference gets it's own RSS feed. This one is also available
  via JSON for an easy way to pull it into the conference site itself.

* All links to posts in the conference feed goes to the conference
  homepage. It's really only designed for transient news.

* Each user also gets it's own RSS feed. This is designed so that it
  can be submitted to an aggregator like Planet PostgreSQL which
  requires personal feeds. Conference specific news are automatically
  prefixed by conference name.

* Each post can individually be toggled if it should be included in
  the RSS feed or not

* Re-adds the news archive, with a paginated view

* Each post can individually be toggled for inclusion in the news
  archive, so it's possible to create more transient news. News
  archive *only* contains news from the global feed, not the
  conference feeds.

* Makes the "read more" button on the frontpage only show up if the
  entire news post did not fit

19 files changed:
media/css/pgeu.css
postgresqleu/confreg/admin.py
postgresqleu/confreg/backendforms.py
postgresqleu/confreg/backendviews.py
postgresqleu/confreg/feeds.py
postgresqleu/confreg/migrations/0028_conferencenews.py [new file with mode: 0644]
postgresqleu/confreg/models.py
postgresqleu/confreg/views.py
postgresqleu/newsevents/admin.py
postgresqleu/newsevents/feeds.py
postgresqleu/newsevents/migrations/0003_conferencenews.py [new file with mode: 0644]
postgresqleu/newsevents/models.py
postgresqleu/newsevents/views.py
postgresqleu/urls.py
postgresqleu/views.py
template/confreg/admin_dashboard_single.html
template/index.html
template/newsevents/archive.html [new file with mode: 0644]
template/newsevents/news.html

index 7c6a42184e031ba1f506165347e5c4672f7d3f12..3363055fe37c255981dfe689a5054bafe4fb905b 100644 (file)
@@ -50,7 +50,7 @@ div.contentwrap h2:after {
     margin: .5em 0 .5em .75em;
 }
 
-h2 a, h3 a {
+h2 a, h3 a, h4 a {
     text-decoration: none;
     color: #515151;
 }
index 0d9f89f0093ac1bf9424d3f0622f41418909df35..9bbb6d4082134588d0b61fddcd51c3712d732806 100644 (file)
@@ -19,6 +19,7 @@ from models import PrepaidVoucher, PrepaidBatch, BulkPayment, DiscountCode
 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
@@ -620,3 +621,4 @@ admin.site.register(AttendeeMail, AttendeeMailAdmin)
 admin.site.register(PendingAdditionalOrder, PendingAdditionalOrderAdmin)
 admin.site.register(VolunteerSlot, VolunteerSlotAdmin)
 admin.site.register(AccessToken)
+admin.site.register(ConferenceNews)
index 6e856f5fbec33fa78095e6a0336823ba3c12f86c..2ca165d12f2d4c37a7124a14920061e3a48725a6 100644 (file)
@@ -22,6 +22,8 @@ from postgresqleu.confreg.models import ConferenceSession, Track, Room
 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
 
@@ -723,6 +725,25 @@ class BackendAccessTokenForm(BackendForm):
                        '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
 #
index fe864b29c8b381d395fa848323591c3e7c1c8715..5f6e8d837ea368e954b5785a425b1e715fbb32d3 100644 (file)
@@ -34,6 +34,7 @@ from backendforms import BackendConferenceSessionSlotForm, BackendVolunteerSlotF
 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:
@@ -504,6 +505,12 @@ def edit_accesstokens(request, urlname, rest):
                                                           BackendAccessTokenForm,
                                                           rest)
 
+def edit_news(request, urlname, rest):
+       return backend_list_editor(request,
+                                                          urlname,
+                                                          BackendNewsForm,
+                                                          rest)
+
 
 ###
 # Non-simple-editor views
index 02710977655609f124a43a933e33415b891e97b0..5809ed0e8b000622589ad2b3510bc4810bff6cfe 100644 (file)
@@ -1,10 +1,13 @@
 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 = "/"
@@ -17,3 +20,33 @@ class LatestEvents(Feed):
        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'])
diff --git a/postgresqleu/confreg/migrations/0028_conferencenews.py b/postgresqleu/confreg/migrations/0028_conferencenews.py
new file mode 100644 (file)
index 0000000..550cb63
--- /dev/null
@@ -0,0 +1,34 @@
+# -*- 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',
+            },
+        ),
+    ]
index 56d5ed2c0f734b13dc4a271d8ad0dae0145e36c9..ae644c6378313f9a7f6963cb9645bf6ea779b2b5 100644 (file)
@@ -23,6 +23,8 @@ from decimal import Decimal
 
 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 = (
@@ -963,3 +965,20 @@ class AccessToken(models.Model):
 
        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'
index e80fb11e47fe3701d06ada45be00a9fcfa9d6a15..8ef8ce3ed6413ff4a7a047f172a73d28087afaf2 100644 (file)
@@ -23,6 +23,7 @@ from models import AttendeeMail, ConferenceAdditionalOption
 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
@@ -61,6 +62,7 @@ from Crypto.Hash import SHA256
 from StringIO import StringIO
 
 import json
+import markdown
 
 #
 # Render a conference page. It will load the template using the jinja system
@@ -168,6 +170,25 @@ def confhome(request, confname):
 
        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):
index 56a20021305afb73d6338c232654c0700c0b8c5f..b95ddf1dff3bd96aaf82f11beafb4b452fe22292 100644 (file)
@@ -1,4 +1,27 @@
 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)
index afbccc4fde9ba7c59f6b30c730eb22e717853524..d323926119361a3434ccb7227b03211b5aa52e17 100644 (file)
@@ -1,19 +1,73 @@
 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'],
+                       )
 
diff --git a/postgresqleu/newsevents/migrations/0003_conferencenews.py b/postgresqleu/newsevents/migrations/0003_conferencenews.py
new file mode 100644 (file)
index 0000000..e0b6be4
--- /dev/null
@@ -0,0 +1,47 @@
+# -*- 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'),
+        ),
+    ]
index 14d6b3c8c568b507fe1a8f278b22ff7316d8bf2c..a0a3ab1a6a37f4e6990d87129bd494c2ddbe8db1 100644 (file)
@@ -1,12 +1,26 @@
 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
index 9b440c744716bc8689a6e9123480ee80c98996d6..22b71b8681b451eaa9b036d71ffe8545e37fbfda 100644 (file)
@@ -1,14 +1,35 @@
 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,
+       })
index d871ba4a8a26b26aeda66e80a8ac05026ab1d596..84ab3856d07c32258e2b96746e525dbe3832312e 100644 (file)
@@ -26,7 +26,7 @@ import postgresqleu.adyen.views
 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
@@ -40,6 +40,7 @@ urlpatterns = [
        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
@@ -50,7 +51,9 @@ urlpatterns = [
        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),
@@ -161,6 +164,7 @@ urlpatterns = [
        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 2cd898c7dc259e97610905d8b39a88035c9cac7a..f90f69dd83d2fa9188b6dd6e6c43c37d6fac8b99 100644 (file)
@@ -1,10 +1,14 @@
 # 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):
@@ -13,7 +17,32 @@ 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,
index b42443e78cdbd5caabb29707714df92590f4798f..fd026a5afe95e4ac4c9ec3958dc24a562e4f7c54 100644 (file)
   <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">
index 51544ae3a40b0333621635cd9d6a3954f91af1bd..e6e84ae668a876f936aad148cba5d28e16b1b5a2 100644 (file)
        <h2 class="text-center">Latest news &nbsp;<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>
diff --git a/template/newsevents/archive.html b/template/newsevents/archive.html
new file mode 100644 (file)
index 0000000..49cf327
--- /dev/null
@@ -0,0 +1,37 @@
+{%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%}">&laquo;</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%}">&raquo;</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%}
index d386cd24a07e57c5dac3aab203183e7d0b17451f..b6fd4aa4cccfa37854e821e14514c216f75a2c19 100644 (file)
@@ -4,7 +4,7 @@
 {%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%}
@@ -13,4 +13,6 @@
 {%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%}