Implement calendar

This commit is contained in:
Yingtong Li 2019-01-11 15:45:11 +11:00
parent c691a7cff2
commit 7ffb586c5b
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
9 changed files with 331 additions and 27 deletions

View File

@ -2,5 +2,5 @@ Django==2.0.6
Jinja2==2.10 Jinja2==2.10
social-auth-app-django==2.1.0 social-auth-app-django==2.1.0
jsonfield==2.0.2 jsonfield==2.0.2
Pillow==3.5.0 Pillow==5.4.1
Markdown==3.0.1 Markdown==3.0.1

View File

@ -2,7 +2,7 @@
{# {#
Society Self-Service Society Self-Service
Copyright © 2018 Yingtong Li (RunasSudo) Copyright © 2018-2019 Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by it under the terms of the GNU Affero General Public License as published by
@ -34,9 +34,8 @@
<div class="item"> <div class="item">
Calendar Calendar
<div class="menu"> <div class="menu">
<a class="item">Your calendar events</a> <a class="{% if request.resolver_match.url_name == 'calendar_list' %}active {% endif %}item" href="{{ url('calendar_list') }}">Your calendar events</a>
<a class="item">Create new calendar event</a> <a class="{% if request.resolver_match.url_name == 'calendar_new' %}active {% endif %}item" href="{{ url('calendar_new') }}">Create new calendar event</a>
<a class="item">Preview the calendar</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,111 @@
{% extends 'sspromotions/base.html' %}
{#
Society Self-Service
Copyright © 2018-2019 Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
#}
{% block title %}{% if request.resolver_match.url_name == 'calendar_new' %}New{% else %}Edit{% endif %} calendar event{% endblock %}
{% block maincontent %}
<h1>{% if request.resolver_match.url_name == 'calendar_new' %}New{% else %}Edit{% endif %} calendar event</h1>
<form class="ui form" method="POST" enctype="multipart/form-data">
<div class="ui disabled inline grid field">
<label class="three wide column">ID</label>
<input class="eleven wide column" type="text" name="id" value="{{ item.id if item.id != None else '' }}">
</div>
<div class="ui required inline grid field">
<label class="three wide column">Title</label>
<input class="eleven wide column" type="text" name="title" value="{{ item.title }}">
</div>
<div class="ui required inline grid field">
<label class="three wide column">Date</label>
<div class="ten wide column">
<div class="ui calendar" id="cal_date">
<div class="ui input left icon grid">
<i class="calendar icon" style="z-index: 999;"></i>
<input class="twelve wide column" type="text" name="date" value="{{ item.date or '' }}">
</div>
</div>
</div>
</div>
<div class="ui required inline grid field">
<label class="three wide column">Group</label>
<select class="ui dropdown eleven wide column" name="group">
<option value="">Group</option>
{% for group in groups %}
<option value="{{ group.id }}">{{ group.name }}</option>
{% endfor %}
</select>
</div>
<div class="ui inline grid field">
<label class="three wide column">Link</label>
<input class="eleven wide column" type="text" name="link" value="{{ item.link or '' }}">
</div>
<div class="ui divider"></div>
<div class="ui inline grid field">
<label class="three wide column">Also limit to</label>
<div class="eleven wide column">
{% for group in all_groups %}
<div class="field" style="display: inline; margin-right: 1em;">
<div class="ui checkbox">
<input type="checkbox" name="also_limit_{{ group.id }}"{% if group.id in item.also_limit %} checked{% endif %}>
<label>{{ group.name }}</label>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="ui divider"></div>
<div class="ui error message"></div>
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
<input class="ui primary button" type="submit" name='submit' value="Save">
<input class="ui button" type="submit" name='submit' value="Save and continue editing">
</form>
{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui-calendar@0.0.8/dist/calendar.min.css" integrity="sha256-KCHiPtYk/vfF5/6lDXpz5r5FuIYchVdai0fepwGft80=" crossorigin="anonymous">
{% endblock %}
{% block script %}
{{ super() }}
<script src="https://cdn.jsdelivr.net/npm/semantic-ui-calendar@0.0.8/dist/calendar.min.js" integrity="sha256-Pnz4CK94A8tUiYWCfg/Ko25YZrHqOKeMS4JDXVTcVA0=" crossorigin="anonymous"></script>
<script>
function leftpad(n) {
if (n < 10)
return '0' + n;
return '' + n;
}
$('#cal_date').calendar({
type: 'date',
formatter: {
date: function(date, settings) {
return date.getFullYear() + '-' + leftpad(date.getMonth() + 1) + '-' + leftpad(date.getDate());
}
}
});
$('.ui.dropdown').dropdown();
{% if item.group %}
$('.ui.dropdown').dropdown('set selected', {{ item.group.id }});
{% endif %}
</script>
{% endblock script %}

View File

@ -0,0 +1,75 @@
{% extends 'sspromotions/base.html' %}
{#
Society Self-Service
Copyright © 2018-2019 Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
#}
{% block title %}Your calendar events{% endblock %}
{% macro listitems(items) %}
<table class="ui selectable celled table">
<thead>
<tr>
<th class="two wide">Date</th>
<th class="eight wide">Title</th>
<th class="two wide">Actions</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td class="selectable"><a href="{{ url('calendar_edit', kwargs={'id': item.id}) }}">{{ item.date }}</a></td>
<td class="selectable"><a href="{{ url('calendar_edit', kwargs={'id': item.id}) }}">{{ item.title }}</a></td>
<td class="selectable">
<a href="{{ url('calendar_edit', kwargs={'id': item.id}) }}" class="ui tiny primary icon button" style="margin: 0.8em 0 0.8em 0.8em;"><i class="edit icon"></i></a>
<a href="{{ url('calendar_delete', kwargs={'id': item.id}) }}" onclick="return confirm('Are you sure you want to delete this calendar event?');" class="ui tiny red icon button" style="margin: 0.8em 0 0.8em 0.8em;"><i class="trash icon"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro %}
{% block maincontent %}
<h1>Your calendar events</h1>
{% if not items_past and not items_upcoming and not items_future %}
<p>You have no calendar events to view. To create a calendar event, click <a href="{{ url('calendar_new') }}">Create new calendar event</a>.</p>
{% endif %}
{% if items_upcoming %}
<h2>Upcoming calendar events (next 14 days)</h2>
{{ listitems(items_upcoming) }}
{% endif %}
{% if items_future %}
<h2>Future calendar events</h2>
{{ listitems(items_future) }}
{% endif %}
{% if items_past %}
<h2>Past calendar events</h2>
{{ listitems(items_past) }}
{% endif %}
{% endblock %}
{% block head %}
{{ super() }}
{% endblock %}

View File

@ -1,6 +1,6 @@
{# {#
Society Self-Service Society Self-Service
Copyright © 2018 Yingtong Li (RunasSudo) Copyright © 2018-2019 Yingtong Li (RunasSudo)
Design by SendWithUs (Apache 2.0 licence) Design by SendWithUs (Apache 2.0 licence)
@ -1353,6 +1353,10 @@
display: block !important; } display: block !important; }
table.menu[align="center"] { table.menu[align="center"] {
width: auto !important; } width: auto !important; }
table .small-margafter {
margin-bottom: 0.8em;
}
</style> </style>
<style type="text/css" media="only screen and (max-width: 596px)"> <style type="text/css" media="only screen and (max-width: 596px)">

View File

@ -2,7 +2,7 @@
{# {#
Society Self-Service Society Self-Service
Copyright © 2018 Yingtong Li (RunasSudo) Copyright © 2018-2019 Yingtong Li (RunasSudo)
Design by SendWithUs (Apache 2.0 licence) Design by SendWithUs (Apache 2.0 licence)
@ -21,21 +21,19 @@
#} #}
{% block content %} {% block content %}
<table class="row masthead"><tbody><tr> <!-- Masthead --> <table class="row"><tbody><tr> <!-- Masthead -->
<th class="small-12 large-12 columns first last" style="padding: 0;"> <th class="small-12 large-12 columns first last">
<table> <table>
<tr> {% for event in events %}
<th> <tr>
<center data-parsed=""> <td class="small-12 large-3">{{ event.date.strftime('%a %d %b') }}</td>
<img src="http://placehold.it/1280x720" valign="bottom" alt="Calendar" align="center" class="text-center"> <td class="small-12 large-9 last small-margafter"><b>{{ event.title }}</b>{% if event.link %} · <a href="{{ event.link }}">Read more &#x203A;</a>{% endif %}</td>
</center> </tr>
</th> {% endfor %}
<th class="expander"></th>
</tr>
</table> </table>
</th> </th>
</tr></tbody></table> </tr></tbody></table>
{% for group in groups %} {% for group in groups if group.bulletinitem_set.filter(date__gte=dtbegin).filter(date__lt=dtend).count() > 0 %}
{% if not loop.first %} {% if not loop.first %}
{{ gap }} {{ gap }}
{% endif %} {% endif %}
@ -52,13 +50,13 @@
</th> </th>
</tr></tbody></table> </tr></tbody></table>
{{ gap }} {{ gap }}
{% for item in group.bulletinitem_set.all() %} {% for item in group.bulletinitem_set.filter(date__gte=dtbegin).filter(date__lt=dtend).all() %}
<table class="row"><tbody><tr> <!-- Main Digest content --> <table class="row"><tbody><tr> <!-- Main Digest content -->
{% if item.image %} {% if item.image %}
<th class="small-12 large-3 columns first"> <th class="small-12 large-3 columns first">
<table> <table>
<tr> <tr>
<th> <th>
{% if item.link %}<a href="{{ item.link }}">{% endif %}<img src="{{ MEDIA_URL }}{{ item.image }}" alt="{{ item.title }}">{% if item.link %}</a>{% endif %} {% if item.link %}<a href="{{ item.link }}">{% endif %}<img src="{{ MEDIA_URL }}{{ item.image }}" alt="{{ item.title }}">{% if item.link %}</a>{% endif %}
</th> </th>
</tr> </tr>

View File

@ -1,5 +1,5 @@
# Society Self-Service # Society Self-Service
# Copyright © 2018 Yingtong Li (RunasSudo) # Copyright © 2018-2019 Yingtong Li (RunasSudo)
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by # it under the terms of the GNU Affero General Public License as published by
@ -51,3 +51,13 @@ class BulletinItem(models.Model):
image = models.ImageField(upload_to='promo_uploads/%Y/%m/%d/', null=True) image = models.ImageField(upload_to='promo_uploads/%Y/%m/%d/', null=True)
content = models.TextField() content = models.TextField()
date = models.DateField() date = models.DateField()
class CalendarItem(models.Model):
group = models.ForeignKey(Group, on_delete=models.CASCADE)
also_limit = JSONField(default=[])
title = models.CharField(max_length=100)
link = models.CharField(max_length=100, null=True)
date = models.DateField()
class Meta:
ordering = ['date', 'title', 'id']

View File

@ -1,5 +1,5 @@
# Society Self-Service # Society Self-Service
# Copyright © 2018 Yingtong Li (RunasSudo) # Copyright © 2018-2019 Yingtong Li (RunasSudo)
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by # it under the terms of the GNU Affero General Public License as published by
@ -24,5 +24,9 @@ urlpatterns = [
path('bulletin/edit/<int:id>', views.bulletin_edit, name='bulletin_edit'), path('bulletin/edit/<int:id>', views.bulletin_edit, name='bulletin_edit'),
path('bulletin/delete/<int:id>', views.bulletin_delete, name='bulletin_delete'), path('bulletin/delete/<int:id>', views.bulletin_delete, name='bulletin_delete'),
path('bulletin/preview/', views.bulletin_preview, name='bulletin_preview'), path('bulletin/preview/', views.bulletin_preview, name='bulletin_preview'),
path('calendar/', views.calendar_list, name='calendar_list'),
path('calendar/new/', views.calendar_new, name='calendar_new'),
path('calendar/edit/<int:id>', views.calendar_edit, name='calendar_edit'),
path('calendar/delete/<int:id>', views.calendar_delete, name='calendar_delete'),
path('', views.index, name='promotions'), path('', views.index, name='promotions'),
] ]

View File

@ -1,5 +1,5 @@
# Society Self-Service # Society Self-Service
# Copyright © 2018 Yingtong Li (RunasSudo) # Copyright © 2018-2019 Yingtong Li (RunasSudo)
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by # it under the terms of the GNU Affero General Public License as published by
@ -23,6 +23,8 @@ from django.utils import timezone
from . import models from . import models
import datetime
@login_required @login_required
def index(request): def index(request):
return render(request, 'sspromotions/index.html') return render(request, 'sspromotions/index.html')
@ -33,12 +35,19 @@ def bulletin_list(request):
items_upcoming = [] items_upcoming = []
items_future = [] items_future = []
dtbegin = timezone.now().date()
dtend = dtbegin + datetime.timedelta(days=7)
for item in models.BulletinItem.objects.all(): for item in models.BulletinItem.objects.all():
if not item.group.can_user_access(request.user): if not item.group.can_user_access(request.user):
continue continue
# TODO if item.date >= dtbegin and item.date < dtend:
items_upcoming.append(item) items_upcoming.append(item)
elif item.date >= dtend:
items_future.append(item)
else:
items_past.append(item)
return render(request, 'sspromotions/bulletin_list.html', { return render(request, 'sspromotions/bulletin_list.html', {
'items_past': items_past, 'items_past': items_past,
@ -48,9 +57,15 @@ def bulletin_list(request):
@login_required @login_required
def bulletin_preview(request): def bulletin_preview(request):
dtbegin = timezone.now().date() + datetime.timedelta(days=1) # Start tomorrow for calendar
dtend = dtbegin + datetime.timedelta(days=14)
return render(request, 'sspromotions/email/bulletin.html', { return render(request, 'sspromotions/email/bulletin.html', {
'groups': [group for group in models.Group.objects.all() if group.bulletinitem_set.count() > 0], 'events': models.CalendarItem.objects.filter(date__gte=dtbegin).filter(date__lt=dtend).all(),
'more': [] 'groups': models.Group.objects.all(),
'more': [],
'dtbegin': dtbegin,
'dtend': dtend
}) })
@login_required @login_required
@ -120,3 +135,91 @@ def bulletin_delete(request, id):
item.delete() item.delete()
return redirect(reverse('bulletin_list')) return redirect(reverse('bulletin_list'))
@login_required
def calendar_list(request):
items_past = []
items_upcoming = []
items_future = []
dtbegin = timezone.now().date() + datetime.timedelta(days=1) # Start tomorrow for calendar
dtend = dtbegin + datetime.timedelta(days=14)
for item in models.CalendarItem.objects.all():
if not item.group.can_user_access(request.user):
continue
if item.date >= dtbegin and item.date < dtend:
items_upcoming.append(item)
elif item.date >= dtend:
items_future.append(item)
else:
items_past.append(item)
return render(request, 'sspromotions/calendar_list.html', {
'items_past': items_past,
'items_upcoming': items_upcoming,
'items_future': items_future
})
@login_required
def calendar_new(request):
if request.method == 'POST':
item = models.CalendarItem()
item.group = models.Group.objects.get(id=int(request.POST['group']))
if not item.group.can_user_access(request.user):
return HttpResponse('Unauthorized', status=401)
item.title = request.POST['title']
item.date = request.POST['date']
item.link = request.POST['link']
item.also_limit = [int(k[11:]) for k, v in request.POST.items() if k.startswith('also_limit_') and v]
item.save()
if request.POST['submit'] == 'Save':
return redirect(reverse('calendar_list'))
else:
return redirect(reverse('calendar_edit', kwargs={'id': item.id}))
else:
item = models.CalendarItem()
item.date = timezone.now()
return render(request, 'sspromotions/calendar_edit.html', {
'item': item,
'groups': [group for group in models.Group.objects.all() if group.can_user_access(request.user)],
'all_groups': models.Group.objects.all()
})
@login_required
def calendar_edit(request, id):
if request.method == 'POST':
item = models.CalendarItem.objects.get(id=id)
item.group = models.Group.objects.get(id=int(request.POST['group']))
if not item.group.can_user_access(request.user):
return HttpResponse('Unauthorized', status=401)
item.title = request.POST['title']
item.date = request.POST['date']
item.link = request.POST['link']
item.also_limit = [int(k[11:]) for k, v in request.POST.items() if k.startswith('also_limit_') and v]
item.save()
if request.POST['submit'] == 'Save':
return redirect(reverse('calendar_list'))
else:
return redirect(reverse('calendar_edit', kwargs={'id': item.id}))
else:
item = models.CalendarItem.objects.get(id=id)
if not item.group.can_user_access(request.user):
return HttpResponse('Unauthorized', status=401)
return render(request, 'sspromotions/calendar_edit.html', {
'item': item,
'groups': [group for group in models.Group.objects.all() if group.can_user_access(request.user)],
'all_groups': models.Group.objects.all()
})
@login_required
def calendar_delete(request, id):
item = models.CalendarItem.objects.get(id=id)
if not item.group.can_user_access(request.user):
return HttpResponse('Unauthorized', status=401)
item.delete()
return redirect(reverse('calendar_list'))