Commit 07a78d1d authored by Holly Elisabeth Oegema's avatar Holly Elisabeth Oegema

Merge branch 'master' into 'update-styles'

# Conflicts:
#   .gitignore
#   README.md
#   layers/views.py
#   scinage/settings.py
#   scinage/settings_local_example.py
#   scinage/urls.py
#   templates/UI/skin.html
#   templates/slides/canvas.html
#   templates/slideshows/slideshows.html
parents fabe641d ca635f38
{
"python.pythonPath": "env/bin/python3.7"
}
\ No newline at end of file
......@@ -23,10 +23,22 @@ To exit virtual env:
`python3 manage.py migrate`
### Install poppler (this is needed for PDF to Slideshow generation)
For Mac:
`brew install poppler`
For Ubuntu 18
`sudo apt-get install poppler-utils`
For Heroku Deployment:
1. `heroku buildpacks:add --index 1 heroku-community/apt`
2. Create a file `Aptfile` which contains
`poppler-utils`
## Running:
`python3 manage.py runserver`
# Contributing
## Compiling SCSS/SASS files:
......@@ -85,59 +97,15 @@ For more details around configuring your CAS system please refer to [the Officia
If you don't want to use a CAS system, there is already an authentication system set up. This will be used by default if no `CAS_SERVER_URL` value is found in settings.
## Using Social Logins
If you want to enable social logins, make sure the following is present in your local settings.
Also be sure to set the following variables in the settings (in settings_local_example by default):
```
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
.....
'social_django', # < -- Add this value
]
MIDDLEWARE_CLASSES = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'social_django.middleware.SocialAuthExceptionMiddleware', # <-- Make sure this value is present
]
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
)
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = '<smtp.gmail.com | OR OTHER HOST>'
EMAIL_USE_TLS = True
EMAIL_PORT = 587
EMAIL_HOST_USER = '<EMAIL ADDRESS>'
EMAIL_HOST_PASSWORD = '<EMAIL PASSWORD>'
```
### Setting up Github Authentication
1. Set up an [OAuth Application on Github](https://github.com/settings/applications/new)
2. `SOCIAL_AUTH_GITHUB_KEY = '<key>'`
3. `SOCIAL_AUTH_GITHUB_SECRET = <secret>`
4. Adding the following to AUTHENTICATION_BACKENDS in settings
```
AUTHENTICATION_BACKENDS = (
....
'social_core.backends.github.GithubOAuth2',
)
```
5. Run `python3 manage.py migrate`
## Configuring Settings
TODO
import json
from .models import *
from django.shortcuts import render
from django.views.decorators.vary import vary_on_cookie
from django.db.models import Count, DateTimeField
from django.db.models.functions import TruncMonth, TruncWeek
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.models import User
from datetime import datetime, timedelta
# number of users
@staff_member_required
def user_metrics(request):
users = User.objects \
.annotate(month=TruncMonth('date_joined')) \
.values('month') \
.annotate(id_count=Count('id')) \
.order_by('month')
user_signup_data = list()
for user in users:
entry = [int(datetime.timestamp(user['month'])*1000), user['id_count']]
user_signup_data.append(entry)
total_user_data = list()
acc_user_count = 0
for data in user_signup_data:
entry = [data[0], data[1] + acc_user_count]
acc_user_count += data[1]
total_user_data.append(entry)
return total_user_data, user_signup_data
# widgets, slides and slideshows created
def query(obj):
return obj.objects \
.annotate(week=TruncWeek('creation_date')) \
.values('week') \
.annotate(id_count=Count('id')) \
.order_by()
def preprocess(input):
output = list()
for data in input:
entry = [int(datetime.timestamp(data['week'])*1000), data['id_count']]
output.append(entry)
return output
@staff_member_required
def creation_metrics(request):
layers = query(Layer)
slides = query(Slide)
slideshows = query(Slideshow)
data_layers = preprocess(layers)
data_slides = preprocess(slides)
data_slideshows = preprocess(slideshows)
return data_layers, data_slides, data_slideshows
#pie chart of owner id and screen connect
@staff_member_required
def screen_pie_metrics(request):
screens_online = Screen.objects \
.filter(last_connect__gte = datetime.now()-timedelta(minutes=0.5))
users = screens_online \
.values('owner_id') \
.annotate(id_count=Count('owner_id')) \
.order_by('-id_count')[:10]
total = 0
for iter in range(0, len(users)):
users[iter]['username'] = str(User.objects.get(pk=users[iter]['owner_id']))
users[iter]['screen_info'] = ""
for screen_info in screens_online.filter(owner_id=users[iter]['owner_id']).values('ip','title'):
users[iter]['screen_info'] += screen_info['title']+'-'+screen_info['ip']+', '
users[iter]['screen_info'] = users[iter]['screen_info'][:-2]
total += users[iter]['id_count']
return users, total
#graph showing number of screens pinged at certain dates
@staff_member_required
def screen_metrics(request):
users = Screen.objects \
.annotate(week=TruncWeek('last_connect')) \
.values('week') \
.annotate(id_count=Count('id')) \
.order_by()
screens = list()
for user in users:
if user['week'] is not None:
entry = [int(datetime.timestamp(user['week'])*1000), user['id_count']]
screens.append(entry)
return screens
#graph showing most-used x widgets
@staff_member_required
def widget_metrics(request):
widgets = list()
for layer in Layer.objects.all():
uses_count = Layer.uses(Layer.objects.get(pk=layer.id))
widgets.append({'count':uses_count, 'title':layer.title})
widgets = sorted(widgets, key=lambda x: x['count'], reverse=True)[:min(15,len(widgets))]
# widgets = SlideLayer.objects \
# .values('title') \
# .annotate(count=Count('slide_id', distinct=True)) \
# .order_by('-count')[:10]
return widgets
@staff_member_required
@vary_on_cookie
def dashboard_all(request):
total_user_data, user_signup_data = user_metrics(request)
data_layers, data_slides, data_slideshows = creation_metrics(request)
screens_pie, total = screen_pie_metrics(request)
screens = screen_metrics(request)
widgets = widget_metrics(request)
return render(request, 'dashboard/dashboard.html',
{'total_users': json.dumps(total_user_data), 'user_signup': json.dumps(user_signup_data),
'layers': json.dumps(data_layers), 'slides': json.dumps(data_slides), 'slideshows': json.dumps(data_slideshows),
'screens_pie': screens_pie, 'total': total, 'screens': json.dumps(screens), 'widgets': widgets})
......@@ -34,7 +34,7 @@ class UserCreateForm(UserCreationForm):
class LocalDateTimeInput(forms.widgets.DateTimeInput):
usel10n=True
class ControlsInlineHelper(FormHelper):
def __init__(self, *args, **kwargs):
......@@ -77,7 +77,6 @@ class LayerForm(forms.ModelForm):
Div('shared', css_class='col-lg-2 col-xs-2'),
# Div('publish', css_class='col-lg-2 col-xs-2'),
css_class='row-fluid'),
HTML("""<div id="changesmodal" style="z-index:30000;" class="modal fade"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button><h4 class="modal-title">Specify Changes</h4></div><div class="modal-body">"""),
HTML("""<p>Specify your changes here. If you keep the 'publish' button checked and save this widget, the widget will be stored as the current 'stable' version of this widget. A new development widget will be created after that which can be worked on.</p>"""),
Fieldset('', 'changes'),
......@@ -92,7 +91,7 @@ class LayerForm(forms.ModelForm):
class Meta:
model = Layer
widgets = {
'description': forms.Textarea(attrs={'rows':2}),
'description': forms.Textarea(attrs={'rows':2}),
}
fields = ["title", "icon", "short", "description", "shared", "tags", "changes", "documentation"]
......@@ -103,9 +102,8 @@ class VersionForm(forms.ModelForm):
self.helper = FormHelper(self)
self.helper.include_media = False
self.helper.form_tag = False
content = forms.CharField(widget=CodeMirrorTextarea(
mode='htmlmixed',
mode='htmlmixed',
dependencies=('javascript', 'xml', 'css'),
config={
'lineWrapping': True
......@@ -215,7 +213,6 @@ class SlideshowSlideForm(forms.ModelForm):
self.fields['length'].widget.attrs['class'] += ' form-control'
self.fields['time_start'].widget.attrs['class'] += ' form-control timeselect'
self.fields['time_end'].widget.attrs['class'] += ' form-control timeselect'
class GroupForm(forms.ModelForm):
......@@ -262,4 +259,10 @@ class ScreenForm(forms.ModelForm):
class Meta:
model = Screen
exclude = ['ip', 'slug', 'owner', 'last_connect']
widgets = {'link': forms.HiddenInput()}
\ No newline at end of file
widgets = {'link': forms.HiddenInput()}
class UploadFileForm(forms.Form):
slideshow_title = forms.CharField(max_length=255)
slide_duration = forms.IntegerField()
file = forms.FileField()
template_name = 'slideshows/create_slideshow_from_pdf.html'
\ No newline at end of file
# Generated by Django 2.2.7 on 2020-02-19 22:20
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('layers', '0037_auto_20191204_0137'),
]
operations = [
migrations.AlterField(
model_name='slidelayer',
name='version',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='layers.Version'),
),
]
......@@ -455,7 +455,7 @@ class SlideLayer(models.Model):
height = models.FloatField(default=20)
title = models.CharField(max_length=100)
index = models.IntegerField()
version = models.ForeignKey(Version, on_delete=models.CASCADE)
version = models.ForeignKey(Version, on_delete=models.CASCADE, blank=True, null=True)
data = models.TextField(blank=True)
def control(self):
......
from django import template
from layers.functions import get_item_permissions, group_perms
from layers.models import FavoriteLayer, LayerRating
from layers.models import FavoriteLayer, LayerRating, SlideshowSlide, Slideshow, Screen
from jinja2 import evalcontextfilter, Markup, escape
register = template.Library()
......@@ -19,11 +19,12 @@ def layer_item_fav(context, layer, mode='manage'):
@register.inclusion_tag('screens/screen_item.html', takes_context=True)
def screen_item(context, screen):
def screen_item(context, screen, domain=""):
return {
'user': context['user'],
'screen': screen,
'perms': get_item_permissions(context['user'], screen)
'domain': domain,
'perms': get_item_permissions(context['user'], screen),
}
......@@ -91,6 +92,35 @@ def sharegroups_single(form):
'form': form
}
@register.inclusion_tag('slides/slide_slideshows.html')
def slide_item_slideshow(slide):
slideshows = SlideshowSlide.objects.filter(slide_id=slide.id)
slideshow_list = list()
slideshow_ids = []
for slideshow in slideshows:
title = Slideshow.objects.filter(id=slideshow.slideshow_id)
if len(title) > 0 and slideshow.slideshow_id not in slideshow_ids:
slideshow_list.append({"title": title[0].title, "id": slideshow.slideshow_id})
slideshow_ids.append(slideshow.slideshow_id)
return {
'slideshow_list': slideshow_list
}
@register.inclusion_tag('slideshows/slideshow_screens.html')
def slideshow_item_screen(slideshow):
slideshow_link = '/slideshows/view/'+str(slideshow.id)+'/'
screens = Screen.objects.filter(link=slideshow_link)
screen_list = list()
for screen in screens:
screen_list.append({"title": screen.title, "id": screen.id})
return {
'screen_list': screen_list
}
##### Filters
......
......@@ -4,7 +4,7 @@ from django.http import JsonResponse, HttpResponse, HttpResponseRedirect, \
from django.utils.html import escape
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.vary import vary_on_cookie
from .models import Slide, InputType, Version, Layer, Control,\
from .models import Slide, Slideshow, InputType, Version, Layer, Control,\
SlideLayer, FavoriteLayer, UploadedImage, Screen, GroupMembership,\
CachedData, UploadedVideo, LayerRating, VersionFeedback
from django.contrib.auth.models import User
......@@ -31,6 +31,13 @@ from django.contrib.auth import logout, authenticate, login, views as auth_views
from layers.forms import UserCreateForm
from django.urls import reverse_lazy
from django.views import generic
from pdf2image import convert_from_bytes
from PIL import Image
from io import BytesIO
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.core.files.base import ContentFile
from io import StringIO
import os
try:
from scinage.emergency import queryEmergency
......@@ -43,6 +50,11 @@ class SignUp(generic.CreateView):
success_url = reverse_lazy('home')
template_name = 'registration/signup.html'
def handler404(request, exception):
response = render(request, 'UI/404.html',)
response.status_code = 404
return response
@login_required
@vary_on_cookie
......@@ -680,9 +692,27 @@ def manageSlides(request):
@login_required
@vary_on_cookie
def listSlides(request, mode):
slides = Slide.objects.filter(owner=request.user).order_by('-edit_date')
slideshow_ids = []
# get list of corresponding slideshows per slide
for slide in slides:
slideshows = SlideshowSlide.objects.filter(slide_id=slide.id)
slideshow_list = list()
for slideshow in slideshows:
title = Slideshow.objects.filter(id=slideshow.slideshow_id)
if len(title) > 0 and slideshow.slideshow_id not in slideshow_ids:
slideshow_list.append({"title": title[0].title, "id": slideshow.slideshow_id})
slideshow_ids.append(slideshow.slideshow_id)
# split slide list into 2 parts for display
slide.slideshow_part1 = slideshow_list[:min(3, len(slideshow_list))]
if len(slideshow_list) >= 3: slide.slideshow_part2 = slideshow_list[3:]
groups = request.user.contentgroup_set.all() | request.user.owned_groups.all()
groups = groups.distinct().annotate(c=Count('slides')).filter(c__gt=0)
return render(request, 'slides/slide_list.html', {'slides': slides, 'groups': groups, 'mode': mode})
......@@ -866,7 +896,10 @@ def viewSlide(request, slide_id):
else:
dimensions = [100.0 * aspect[0] / aspect[1], 100.0 * aspect[1] / aspect[0]]
for instance in slide.slidelayer_set.all():
slidelayers.append((instance, renderSlideLayer(instance.version.id, instance.data)))
if instance.version is None:
slidelayers.append((instance, renderSlideLayer(None, instance.data)))
else:
slidelayers.append((instance, renderSlideLayer(instance.version.id, instance.data)))
content = loader.render_to_string(
'slides/sliderender.html',
{'slide': slide, 'slidelayers': slidelayers, 'dimensions': dimensions},
......@@ -900,6 +933,8 @@ def previewLayer(request):
def renderSlideLayer(versionid, data):
if versionid == None:
return data
version = Version.objects.get(pk=versionid)
try:
data = json.loads(data)
......@@ -929,10 +964,87 @@ def manageSlideshows(request):
@vary_on_cookie
def listSlideshows(request, mode):
slideshows = Slideshow.objects.filter(owner=request.user).order_by('-edit_date')
groups = request.user.contentgroup_set.all() | request.user.owned_groups.all()
groups = groups.distinct().annotate(c=Count('slideshows')).filter(c__gt=0)
return render(request, 'slideshows/slideshow_list.html', {'slideshows': slideshows, 'mode': mode, 'groups': groups})
def convert_pdf_to_slideshow(request, f):
base_image_name, extension = os.path.splitext(f.name)
# Check the extension to see if the file is a PDF
# if it's not reject and return False and the error message
if extension != ".pdf":
return False, -1, "This file is not a PDF. Only PDFs are accepted."
# Retrieving slide titles and slide durations from the form
slideshow_title = base_image_name if request.POST["slideshow_title"] == "" else request.POST["slideshow_title"]
slide_duration = request.POST['slide_duration']
slideshow_description = f"Slideshow created from {base_image_name}" if request.POST["slideshow_description"] == "" else request.POST["slideshow_description"]
# Convert the pdf to images
images = convert_from_bytes(f.read(), fmt="png")
# Create Slide Show Object
slideshow = Slideshow.objects.create(owner=request.user, title=slideshow_title, description=slideshow_description)
slideshow.save()
slideshow_id = slideshow.id
# Upload each image
# Create a slide for each image
# Create a slide layer for the slide containing the uploaded image
# Add the slide to the slideshow
for count, image in enumerate(images):
# Create image name from the name and count
image_name = f"uploaded_images/{base_image_name}_{count}.png" # Create image name using count + file name
# Things break if the media url has a slash at the beginning (ie. /media/)
# Handling this case
media_url = settings.MEDIA_URL if settings.MEDIA_URL[0] != "/" else settings.MEDIA_URL[1:]
full_image_path = f"{media_url}{image_name}"
image.save(full_image_path, 'PNG')
# Save the image in the database
uim = UploadedImage.objects.create(image=image_name, owner=request.user)
uim.save()
# Create a Slide
slide = Slide.objects.create(owner=request.user, title=f"{base_image_name}_{count}", description=f"{uim.image.url}", data="")
slide.save()
slideLayer = SlideLayer.objects.create(slide=slide, title="Image", data=f"<img style=\"max-width: 80%; max-height: 80%; object-fit: contain;\" src=\"../../../{full_image_path}\">", index=1, x=10, y=0, width=100, height=100, version_id=None)
slideLayer.save()
# Adding Slide to Slideshow
slideshowSlide = SlideshowSlide.objects.create(slideshow=slideshow, slide=slide, order=count, length=slide_duration)
slideshowSlide.save()
# If no errors occurred, return True to indicate
# a successful run
return True, slideshow_id, "",
def uploadPDF(request):
if request.method == 'POST':
form = UploadFileForm(request.POST, request.FILES)
if request.FILES['document']:
success, slideshow_id, error_msg = convert_pdf_to_slideshow(request, request.FILES['document'])
# Redirect to the completed slideshow if successful
if success:
return HttpResponseRedirect(f'/slideshows/edit/{slideshow_id}/')
else:
# Rerender the page but with the error message
return render(request, 'slideshows/create_slideshow_from_pdf.html', {'error': True, 'error_msg': error_msg})
else:
form = UploadFileForm()
return HttpResponseRedirect('/slideshows/pdf/')
@login_required
def createSlideshowPDF(request):
return render(request, 'slideshows/create_slideshow_from_pdf.html')
@login_required
@vary_on_cookie
......@@ -1001,6 +1113,7 @@ def viewSlideshow(request, slideshow_id):
dimensions = [100.0 * aspect[0] / aspect[1], 100.0 * aspect[1] / aspect[0]]
else:
dimensions = [100, 56.25]
return render(request, 'slideshows/slideshow_no_slides_found.html')
return render(request, 'slideshows/slideshowrender.html', {'dimensions': dimensions, 'slideshow': slideshow, 'fs_src': '/slides/view/%s/' % first_slide.slide.id, 'fs_time': first_slide.length * 1000})
......@@ -1327,6 +1440,12 @@ def viewScreen(request, slug):
def pingScreen(request, slug):
screen = get_object_or_404(Screen, slug=slug)
now = timezone.now()
linksplit = screen.link.split('/')
itemtype = linksplit[1]
slide_has_updated = False
if itemtype == 'slides':
slide = Slide.objects.get(pk=int(linksplit[3]))
slide_has_updated = screen.last_connect < slide.edit_date < now
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip_address = x_forwarded_for.split(',')[0]
......@@ -1335,9 +1454,10 @@ def pingScreen(request, slug):
screen.last_connect = now
screen.ip = ip_address
screen.save()
emergency = queryEmergency(request)
if emergency:
return emergency
if settings.EMERGENCY_ALERTS_ENABLED:
emergency = queryEmergency(request)
if emergency:
return emergency
return JsonResponse({'target': screen.link})
#trashbin
......
......@@ -23,6 +23,7 @@ libsass==0.19.4
lxml==4.4.1
MarkupSafe==1.1.1
oauthlib==3.1.0
pdf2image==1.11.0
pefile==2019.4.18
pilkit==2.0
Pillow==6.2.1
......
......@@ -38,6 +38,7 @@ INSTALLED_APPS = (
)
MIDDLEWARE = [
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
......@@ -59,8 +60,6 @@ TEMPLATES = [
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'scinage.context_processors.newlysaved',
'social_django.context_processors.backends',
'social_django.context_processors.login_redirect',
],
},
},
......@@ -105,10 +104,3 @@ DOC_URL = '/static/documentation/index.html'
COMPRESS_ROOT = '/static/css/'
LOGIN_REDIRECT_URL ='/'
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = '<smtp.gmail.com | OR OTHER HOST>'
EMAIL_USE_TLS = True
EMAIL_PORT = 587
EMAIL_HOST_USER = '<EMAIL ADDRESS>'
EMAIL_HOST_PASSWORD = '<EMAIL PASSWORD>'
......@@ -50,8 +50,8 @@ AUTHENTICATION_BACKENDS = (
)
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',