Last active
February 7, 2024 11:20
-
-
Save amitu/a1ba8b61cb9359f618d39d84c5f6b5ce to your computer and use it in GitHub Desktop.
slides dj stuff
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from django import forms | |
from django.template.defaultfilters import slugify | |
import django | |
import fastn.django | |
from . import models | |
from .forms import VisibilityField | |
from slides import renderer_utils | |
import subprocess | |
def create_blank_template(request: django.http.HttpRequest): | |
result = subprocess.run(["bash", "../scripts/create-blank-template.sh"]) | |
exit_code = result.returncode | |
if exit_code != 0: | |
return django.http.HttpResponse("Blank template created successfully.") | |
else: | |
err = result.stderr | |
return django.http.HttpResponse( | |
f"Blank Template Creation error: {err}, code: {exit_code}" | |
) | |
@fastn.django.action | |
class CreatePresentation(fastn.django.Form): | |
title = forms.CharField(max_length=200) | |
template_slug = forms.SlugField() | |
team_slug = forms.SlugField() | |
target_team = forms.SlugField() | |
visibility = VisibilityField() | |
def clean_target_team(self): | |
target_team = self.cleaned_data["target_team"] | |
if self.request.user.is_anonymous: | |
raise forms.ValidationError("User not logged in") | |
try: | |
target_team = models.Org.objects.get(slug=target_team) | |
except models.Org.DoesNotExist: | |
if target_team == self.request.user.username: | |
target_team = models.Org.create( | |
name=self.request.user.username, | |
slug=self.request.user.username, | |
owner=self.request.user, | |
) | |
else: | |
raise forms.ValidationError("Team does not exist") | |
# TODO: ensure user has write access to the team | |
# tm = models.TeamMember.objects.get( | |
# team=target_team, user=self.request.user | |
# ) # raises 404 if not found | |
# if tm.role == models.TeamMember.ROLE_GUEST: | |
# # Guests can not create presentation, only participate in existing ones | |
# raise django.http.Http404("User is not authenticated") | |
return target_team | |
def clean_team_slug(self): | |
team_slug = self.cleaned_data["team_slug"] | |
template_slug = self.cleaned_data["template_slug"] | |
try: | |
self.template = models.Presentation.objects.get( | |
team__slug=team_slug, slug=template_slug | |
) | |
except models.Presentation.DoesNotExist: | |
raise forms.ValidationError( | |
f"Template {team_slug}/{template_slug} does not exist" | |
) | |
return team_slug | |
def save(self): | |
print("CreatePresentation.save") | |
new_presentation = models.Presentation.objects.create( | |
title=self.cleaned_data["title"], | |
# TODO: the following can raise an exception due to unique constraint, handle it. | |
slug=slugify(self.cleaned_data["title"]), | |
team=self.cleaned_data["target_team"], | |
owner=self.request.user, | |
setting_ftd=self.template.setting_ftd, | |
fastn_ftd=self.template.fastn_ftd, | |
status="draft", | |
is_template=False, | |
visibility=self.cleaned_data["visibility"], | |
) | |
# Copy slides from the template to the new presentation | |
for template_slide in self.template.slides.order_by("order"): | |
models.Slide.objects.create( | |
title=template_slide.title, | |
slug=template_slide.slug, | |
snapshot=template_slide.snapshot, | |
presentation=new_presentation, | |
order=template_slide.order, | |
content=template_slide.content, | |
status="draft", | |
) | |
# Create the response data | |
return fastn.django.redirect(new_presentation.presentation_url()) | |
@fastn.django.action | |
class CreateSlide(fastn.django.Form): | |
presentation_slug = forms.SlugField() | |
team_slug = forms.SlugField() | |
template_slide_slug = forms.SlugField() | |
template_presentation_slug = forms.SlugField() | |
def clean(self): | |
presentation_slug = self.cleaned_data.get("presentation_slug") | |
team_slug = self.cleaned_data.get("team_slug") | |
if self.request.user.is_anonymous: | |
raise forms.ValidationError("User not logged in") | |
try: | |
self.presentation = models.Presentation.objects.get( | |
slug=presentation_slug, team__slug=team_slug | |
) | |
except models.Presentation.DoesNotExist: | |
raise forms.ValidationError( | |
f"Presentation {presentation_slug} in team {team_slug} does not exist" | |
) | |
template_slide_slug = self.cleaned_data.get("template_slide_slug") | |
template_presentation_slug = self.cleaned_data.get("template_presentation_slug") | |
try: | |
self.template_slide = models.Slide.objects.get( | |
slug=template_slide_slug, | |
presentation__slug=template_presentation_slug, | |
presentation__is_template=True, | |
) | |
except models.Slide.DoesNotExist: | |
raise forms.ValidationError( | |
f"Template Slide {template_slide_slug} in " | |
f"presentation {template_presentation_slug}" | |
f"does not exist" | |
) | |
def next_slide_order(self): | |
current_max_ordered_slide = ( | |
models.Slide.objects.filter(presentation=self.presentation) | |
.order_by("-order") | |
.first() | |
) | |
order = 1 | |
if current_max_ordered_slide is not None: | |
# At-least one slide exists for this presentation | |
order = current_max_ordered_slide.order + 1 | |
return order | |
def save(self): | |
# TODO: ensure user has write access to the team | |
order = self.next_slide_order() | |
slides_count = models.Slide.objects.filter( | |
presentation=self.presentation | |
).count() | |
default_title = f"Untitled {slides_count + 1}" | |
new_slide = models.Slide.objects.create( | |
title=default_title, | |
slug=slugify(default_title), | |
snapshot=self.template_slide.snapshot, | |
presentation=self.presentation, | |
order=order, | |
content=self.template_slide.content, | |
) | |
new_slide.save() | |
return fastn.django.redirect(self.presentation.presentation_url(order)) | |
@fastn.django.action | |
class MoveSlide(fastn.django.Form): | |
presentation_slug = forms.SlugField() | |
team_slug = forms.SlugField() | |
order = forms.IntegerField() | |
# todo: need to change this direction to enum field | |
direction = forms.CharField(max_length=10) | |
def clean(self): | |
presentation_slug = self.cleaned_data.get("presentation_slug") | |
team_slug = self.cleaned_data.get("team_slug") | |
if self.request.user.is_anonymous: | |
raise forms.ValidationError("User not logged in") | |
try: | |
self.presentation = models.Presentation.objects.get( | |
slug=presentation_slug, team__slug=team_slug | |
) | |
except models.Presentation.DoesNotExist: | |
raise forms.ValidationError( | |
f"Presentation {presentation_slug} in team {team_slug} does not exist" | |
) | |
def swap_slide_orders(self, a, b): | |
# todo: need to swap these unique value fields in a better way | |
original_a_order = a.order | |
original_b_order = b.order | |
a.order = -1 | |
a.save() | |
b.order = original_a_order | |
b.save() | |
a.order = original_b_order | |
a.save() | |
def save(self): | |
presentation_slug = self.cleaned_data.get("presentation_slug") | |
direction = self.cleaned_data.get("direction") | |
if direction == "none": | |
# if direction is not specified | |
return django.http.HttpResponse("No slide changes") | |
all_slides = models.Slide.objects.filter( | |
presentation=self.presentation | |
).order_by("order") | |
slides_count = len(all_slides) | |
order = self.cleaned_data.get("order") | |
# Case 1: | |
# current state -> 1 2 3 4 5 | |
# if corner slides are moved to out of bound direction (just ignore this) | |
# lets say we are trying to move slide 1 to left which makes no sense | |
# but if someone do try it then we should ignore it. | |
if order == 1 and direction == "left": | |
return django.http.HttpResponse( | |
"Cant move leftmost slide to left (no changes)" | |
) | |
if order == slides_count and direction == "right": | |
return django.http.HttpResponse( | |
"Cant move rightmost slide to right (no changes)" | |
) | |
current_slide = models.Slide.objects.get( | |
presentation=self.presentation, order=order | |
) | |
# Case 2: | |
# current state -> 1 2 3 4 5 | |
# let's say we move slide 4 to left | |
# (*if slide left to current slide exists) | |
# final state -> 1 2 4 3 5 | |
if direction == "left": | |
left_slide_order = order - 1 | |
try: | |
left_slide = models.Slide.objects.get( | |
presentation=self.presentation, order=left_slide_order | |
) | |
self.swap_slide_orders(left_slide, current_slide) | |
except models.Slide.DoesNotExist: | |
raise forms.ValidationError( | |
f"Slide with order {left_slide_order} in presentation {presentation_slug} does not " | |
f"exist" | |
) | |
# Case 3: | |
# current state -> 1 2 3 4 5 | |
# let's say we move slide 4 to right | |
# (*if slide right to current slide exists) | |
# final state -> 1 2 3 5 4 | |
if direction == "right": | |
right_slide_order = order + 1 | |
try: | |
right_slide = models.Slide.objects.get( | |
presentation=self.presentation, order=right_slide_order | |
) | |
self.swap_slide_orders(right_slide, current_slide) | |
except models.Slide.DoesNotExist: | |
raise forms.ValidationError( | |
f"Slide with order {right_slide_order} in presentation {presentation_slug} does not " | |
f"exist" | |
) | |
# todo: remain in the current slide page | |
# if the current slide order is affected | |
# currently reloading the page | |
return fastn.django.reload() | |
@fastn.django.action | |
class DeleteSlide(fastn.django.Form): | |
presentation_slug = forms.SlugField() | |
team_slug = forms.SlugField() | |
order = forms.IntegerField() | |
def clean(self): | |
presentation_slug = self.cleaned_data.get("presentation_slug") | |
team_slug = self.cleaned_data.get("team_slug") | |
if self.request.user.is_anonymous: | |
raise forms.ValidationError("User not logged in") | |
try: | |
self.presentation = models.Presentation.objects.get( | |
slug=presentation_slug, team__slug=team_slug | |
) | |
except models.Presentation.DoesNotExist: | |
raise forms.ValidationError( | |
f"Presentation {presentation_slug} does not exist" | |
) | |
def save(self): | |
presentation_slug = self.cleaned_data.get("presentation_slug") | |
team_slug = self.cleaned_data.get("team_slug") | |
slide_order = self.cleaned_data.get("order") | |
all_slides = models.Slide.objects.filter( | |
presentation=self.presentation | |
).order_by("order") | |
is_slide_deleted = False | |
# Lesser than or just after the deleted one | |
existing_slide_order = None | |
for s in all_slides: | |
current_order = s.order | |
if current_order == slide_order: | |
render_id = s.ekey | |
# Delete slide object | |
s.delete() | |
# Delete rendered content for this slide (if exists) | |
renderer_utils.delete_rendered_content(render_id) | |
is_slide_deleted = True | |
if current_order > slide_order and is_slide_deleted: | |
s.order = current_order - 1 | |
s.save() | |
all_slides = models.Slide.objects.filter( | |
presentation=self.presentation | |
).order_by("order") | |
for s in all_slides: | |
current_slide_order = s.order | |
if existing_slide_order is None: | |
existing_slide_order = current_slide_order | |
if current_slide_order <= slide_order: | |
existing_slide_order = current_slide_order | |
# If no slide found after deleting then redirect to blank slide 0 | |
if existing_slide_order is None: | |
existing_slide_order = 0 | |
return fastn.django.redirect( | |
f"/p/{team_slug}/{presentation_slug}/{existing_slide_order}" | |
) | |
@fastn.django.action | |
class SaveSlide(fastn.django.Form): | |
team_slug = forms.SlugField() | |
presentation_slug = forms.SlugField() | |
order = forms.IntegerField() | |
content = forms.CharField() | |
def clean_content(self): | |
try: | |
self.presentation = models.Presentation.objects.get( | |
team__slug=self.cleaned_data["team_slug"], | |
slug=self.cleaned_data["presentation_slug"], | |
) | |
except models.Presentation.DoesNotExist: | |
raise forms.ValidationError("Presentation does not exist") | |
# TODO: check if user has write permission to this presentation | |
try: | |
self.slide = models.Slide.objects.get( | |
presentation=self.presentation, | |
order=self.cleaned_data["order"], | |
) | |
except models.Slide.DoesNotExist: | |
raise forms.ValidationError("Slide does not exist") | |
content = self.cleaned_data["content"] | |
self.render(content) | |
# todo: use fastn build to see if the content is valid | |
return content | |
def render(self, content): | |
from slides import renderer_utils | |
fastn_ftd = self.presentation.fastn_ftd | |
setting_ftd = self.presentation.setting_ftd | |
# render and updates self.slide_preview and self.slide_thumbnail | |
# todo: send slide ekey as render_id | |
self.render_success, snapshot_or_error_bytes = renderer_utils.render( | |
"bsj76t3ev8S", fastn_ftd, setting_ftd, content | |
) | |
if self.render_success: | |
print("RENDER SUCCESS") | |
self.snapshot_bytes = snapshot_or_error_bytes | |
else: | |
print("RENDER FAILURE") | |
self.error_bytes = snapshot_or_error_bytes | |
def save(self): | |
print(f"Render status: {self.render_success}") | |
if not self.render_success: | |
error_response = {} | |
if self.error_bytes is not None: | |
print("Error bytes found") | |
error_response["errors"] = { | |
"content": self.error_bytes.decode("utf-8"), | |
} | |
else: | |
print("No error bytes found") | |
return django.http.JsonResponse(error_response, status=200) | |
# all data is valid, only save updates database | |
# Save slide content | |
self.slide.content = self.cleaned_data["content"] | |
# TODO: before first save, the self.snapshot is the shared version, post that we have to create | |
# a new snapshot for this slide. Lets add .cloned boolean field in Snapshot table. | |
snapshot = models.SlideSnapshot.objects.create( | |
preview=self.snapshot_bytes, thumbnail=self.snapshot_bytes | |
) | |
self.slide.snapshot = snapshot | |
self.slide.save() | |
return fastn.django.reload() | |
@fastn.django.action | |
class ToggleTemplate(fastn.django.Form): | |
team_slug = forms.SlugField() | |
presentation_slug = forms.SlugField() | |
def clean_team_slug(self): | |
team_slug = self.cleaned_data.get("team_slug") | |
try: | |
self.team = models.Org.objects.get( | |
slug=team_slug, | |
) | |
except models.Org.DoesNotExist: | |
raise forms.ValidationError(f"Team {team_slug} does not exist") | |
return team_slug | |
def clean(self): | |
presentation_slug = self.cleaned_data.get("presentation_slug") | |
team_slug = self.cleaned_data.get("team_slug") | |
try: | |
self.presentation = models.Presentation.objects.get( | |
slug=presentation_slug, team__slug=team_slug | |
) | |
except models.Presentation.DoesNotExist: | |
raise forms.ValidationError( | |
f"Presentation {presentation_slug} for team {team_slug} does not exist" | |
) | |
def save(self): | |
self.presentation.is_template = not self.presentation.is_template | |
self.presentation.save() | |
return fastn.django.reload() | |
@fastn.django.action | |
class SavePresentationSettings(fastn.django.Form): | |
team_slug = forms.SlugField() | |
presentation_slug = forms.SlugField() | |
fastn_conf = forms.CharField() | |
settings_conf = forms.CharField() | |
def clean_team_slug(self): | |
team_slug = self.cleaned_data.get("team_slug") | |
try: | |
self.team = models.Org.objects.get( | |
slug=team_slug, | |
) | |
except models.Org.DoesNotExist: | |
raise forms.ValidationError(f"Team {team_slug} does not exist") | |
return team_slug | |
def clean(self): | |
presentation_slug = self.cleaned_data.get("presentation_slug") | |
team_slug = self.cleaned_data.get("team_slug") | |
# todo: there is no check for fastn and settings data yet | |
# will be validating it later | |
try: | |
self.presentation = models.Presentation.objects.get( | |
slug=presentation_slug, team__slug=team_slug | |
) | |
except models.Presentation.DoesNotExist: | |
raise forms.ValidationError( | |
f"Presentation {presentation_slug} for team {team_slug} does not exist" | |
) | |
def save(self): | |
self.presentation.fastn_ftd = self.cleaned_data.get("fastn_conf") | |
self.presentation.setting_ftd = self.cleaned_data.get("settings_conf") | |
self.presentation.save() | |
return fastn.django.reload() | |
@fastn.django.action | |
class ChangePresentationTitle(fastn.django.Form): | |
team_slug = forms.SlugField() | |
presentation_slug = forms.SlugField() | |
title = forms.CharField() | |
def clean_team_slug(self): | |
team_slug = self.cleaned_data.get("team_slug") | |
try: | |
self.team = models.Org.objects.get( | |
slug=team_slug, | |
) | |
except models.Org.DoesNotExist: | |
raise forms.ValidationError(f"Team {team_slug} does not exist") | |
return team_slug | |
def clean(self): | |
presentation_slug = self.cleaned_data.get("presentation_slug") | |
team_slug = self.cleaned_data.get("team_slug") | |
team_name = self.team.name | |
self.new_presentation_title = self.cleaned_data.get("title") | |
self.new_presentation_slug = slugify(self.new_presentation_title) | |
if len(self.new_presentation_title) == 0: | |
empty_title_error_response = { | |
"errors": { | |
"title": "Presentation Title is empty", | |
} | |
} | |
self.error = empty_title_error_response | |
return | |
try: | |
self.presentation = models.Presentation.objects.get( | |
slug=self.new_presentation_slug, team__slug=team_slug | |
) | |
duplicate_presentation_error_response = { | |
"errors": { | |
"title": f"Presentation with this title " | |
f"already exists for the " | |
f"current team {team_name}. Choose a different title", | |
} | |
} | |
self.error = duplicate_presentation_error_response | |
return | |
except models.Presentation.DoesNotExist: | |
try: | |
self.presentation = models.Presentation.objects.get( | |
slug=presentation_slug, team__slug=team_slug | |
) | |
except models.Presentation.DoesNotExist: | |
raise forms.ValidationError( | |
f"Presentation {presentation_slug} for team {team_slug} does not exist" | |
) | |
def save(self): | |
if getattr(self, "error", None) is not None: | |
return django.http.JsonResponse(self.error, status=200) | |
team_slug = self.cleaned_data.get("team_slug") | |
self.presentation.title = self.new_presentation_title | |
self.presentation.slug = self.new_presentation_slug | |
self.presentation.save() | |
return fastn.django.redirect( | |
f"/p/{team_slug}/{self.new_presentation_slug}/settings/" | |
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from django import forms | |
from slides import models | |
class VisibilityField(forms.CharField): | |
def validate(self, value): | |
if value not in [ | |
models.VISIBILITY_EVERYONE, | |
models.VISIBILITY_TEAM, | |
models.VISIBILITY_ONLY_ME, | |
]: | |
raise forms.ValidationError( | |
f"Invalid visibility value, {value}, " | |
f"allowed values: {models.VISIBILITY_EVERYONE}, {models.VISIBILITY_TEAM}, {models.VISIBILITY_ONLY_ME}" | |
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from django.db import models | |
from ft.models import User, Org | |
from encrypted_id.models import EncryptedIDModel | |
from slides import utils | |
VISIBILITY_EVERYONE = "everyone" | |
VISIBILITY_ORG = "org" | |
VISIBILITY_ONLY_ME = "only_me" | |
class SlideSnapshot(models.Model): | |
preview = models.BinaryField() # this is the PNG content | |
thumbnail = models.BinaryField() # this is the PNG content | |
class Meta: | |
db_table = "slides_slide_snapshot" | |
class OrgTemplate(models.Model): | |
org = models.ForeignKey(Org, on_delete=models.PROTECT, related_name="+") | |
template = models.ForeignKey( | |
"Presentation", on_delete=models.PROTECT, related_name="+" | |
) | |
custom_data = models.JSONField(default=dict) | |
# Needs Org, User | |
class Presentation(EncryptedIDModel): | |
# template = models.ForeignKey("Presentation", on_delete=models.PROTECT, related_name="+") | |
# `snapshot` is redundant, but it's a performance optimization | |
# snapshot = models.ForeignKey(SlideSnapshot, on_delete=models.PROTECT, related_name="+") | |
title = models.TextField(max_length=200) | |
slug = models.SlugField(max_length=200) | |
org = models.ForeignKey(Org, on_delete=models.PROTECT, related_name="+") | |
owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name="+") | |
# we will store a `settings.ftd` file in the database, it will be auto imported in each slide, and can define | |
# color scheme and other global data. | |
setting_ftd = models.TextField() # FTD | |
fastn_ftd = models.TextField() # FTD | |
created_at = models.DateTimeField(auto_now_add=True) | |
updated_at = models.DateTimeField(auto_now=True) | |
STATUS_CHOICES = [ | |
("draft", "Draft"), | |
("approved", "Approved"), | |
] | |
status = models.TextField(max_length=10, choices=STATUS_CHOICES, default="draft") | |
issue_count = models.IntegerField(default=0) # can be calculated from comments | |
is_template = models.BooleanField(default=False) | |
# only me, with Org, with everyone | |
VISIBILITY_CHOICES = [ | |
(VISIBILITY_ONLY_ME, "Only Me"), | |
(VISIBILITY_ORG, "Org"), | |
(VISIBILITY_EVERYONE, "Everyone"), | |
] | |
visibility = models.TextField( | |
max_length=10, choices=VISIBILITY_CHOICES, default=VISIBILITY_ORG | |
) | |
class Meta: | |
db_table = "slides_presentation" | |
unique_together = [("org", "slug")] | |
def dashboard_view(self): | |
return { | |
"title": self.title, | |
"thumbnail": self.thumbnail_url(), | |
"updated-on": "Edited 18 min ago", # TODO: calculate this | |
"url": self.presentation_url(), | |
} | |
def thumbnail_url(self): | |
return self.slides.get(order=1).thumbnail_url() | |
def preview_url(self): | |
return self.slides.get(order=1).preview_url() | |
def to_public_presentation(self): | |
# record definition in templates.ftd | |
return { | |
"id": self.id, | |
"url": self.template_url(), | |
"title": self.title, | |
"org": self.org.name, | |
"owner_name": self.owner.username, | |
"thumbnail": self.thumbnail_url(), | |
"owner_avatar": "/-/ui.fifthtry.com/slides/assets/avatar.svg", | |
} | |
def to_detail_presentation(self): | |
# record definition in template.ftd | |
""" | |
:return: | |
caption template-name: x | |
string template-slug: x | |
integer id: x | |
string owner-name: x | |
string org-name: x | |
string org-slug: x | |
string org-initials: x | |
string preview: x | |
string thumbnail: x | |
string org-avatar: x? | |
string list slides: x | |
""" | |
return { | |
"id": self.id, | |
"template-name": self.title, | |
"org-name": self.org.name, | |
"owner-name": self.owner.username, | |
"owner-initials": utils.get_initials(self.owner.username), | |
"org-slug": self.org.slug, | |
"org-initials": utils.get_initials(self.org.name), | |
"template-slug": self.slug, | |
"preview": self.preview_url(), | |
"thumbnail": self.thumbnail_url(), | |
"org-avatar": "/-/ui.fifthtry.com/slides/assets/avatar.svg", | |
"slides": [s.thumbnail_url() for s in self.slides.all()], | |
} | |
def get_slide_by_order(self, order): | |
return Slide.objects.get(presentation=self, order=order) | |
def __str__(self) -> str: | |
return "Id: " + str(self.id) + " " + self.title | |
def template_url(self) -> str: | |
return f"/t/{self.org.slug}/{self.slug}/" | |
def presentation_url(self, order=1) -> str: | |
if order == 1: | |
return f"/p/{self.org.slug}/{self.slug}/" | |
else: | |
return f"/p/{self.org.slug}/{self.slug}/{order}/" | |
def settings_url(self) -> str: | |
return f"/p/{self.org.slug}/{self.slug}/settings/" | |
def present_url(self) -> str: | |
return f"/p/{self.org.slug}/{self.slug}/present/" | |
class RawAsset(models.Model): | |
content = models.BinaryField() # this is the PNG content | |
dark_content = models.BinaryField() # this is the PNG content | |
class Meta: | |
db_table = "slides_raw_asset" | |
class Asset(models.Model): | |
presentation = models.ForeignKey( | |
Presentation, on_delete=models.PROTECT, related_name="assets" | |
) | |
# org = models.ForeignKey(Org, on_delete=models.PROTECT, related_name="+") | |
name = models.TextField(max_length=200) # this will be the root relative file name | |
# we will store the dark version | |
raw = models.ForeignKey(RawAsset, on_delete=models.PROTECT, related_name="+") | |
created_at = models.DateTimeField(auto_now_add=True) | |
updated_at = models.DateTimeField(auto_now=True) | |
visibility = models.TextField( | |
max_length=10, choices=Presentation.VISIBILITY_CHOICES, default=VISIBILITY_ORG | |
) | |
class Meta: | |
db_table = "slides_asset" | |
def url(self): | |
return "/asset/%s.png" % self.ekey | |
class Slide(EncryptedIDModel): | |
# template = models.ForeignKey("Slide", on_delete=models.PROTECT, related_name="+") | |
# this is the last successful snapshot, in case of error we will not update this. | |
snapshot = models.ForeignKey( | |
SlideSnapshot, on_delete=models.PROTECT, related_name="snapshots" | |
) | |
# this will be template presentation for slide templates | |
presentation = models.ForeignKey( | |
Presentation, on_delete=models.PROTECT, related_name="slides" | |
) | |
title = models.TextField(max_length=200) | |
slug = models.SlugField(max_length=200) | |
order = models.IntegerField() | |
content = models.TextField() # FTD | |
""" | |
-- import: foo | |
-- foo.slide-1: Hello | |
This is a slide. | |
""" | |
# if there is any error in the content we will store it here. | |
error = models.TextField(null=True) | |
STATUS_CHOICES = [ | |
("draft", "Draft"), | |
("approved", "Approved"), | |
] | |
status = models.TextField(max_length=10, choices=STATUS_CHOICES, default="draft") | |
created_at = models.DateTimeField(auto_now_add=True) | |
updated_at = models.DateTimeField(auto_now=True) | |
issue_count = models.IntegerField(default=0) # can be calculated from comments | |
class Meta: | |
db_table = "slides_slide" | |
unique_together = [("presentation", "order")] | |
def to_detail_slide(self): | |
return { | |
"content": self.content, | |
"preview-url": self.preview_url(), | |
# todo: "is-valid": not self.error, | |
"is-valid": True, | |
} | |
def to_slide_thumbnail(self): | |
return { | |
"url": self.presentation.presentation_url(self.order), | |
"thumbnail-url": self.thumbnail_url(), | |
# todo: "is-valid": not self.error, | |
"is-valid": True, | |
} | |
def to_template_slide_data(self): | |
return { | |
"title": self.title, | |
"slug": self.slug, | |
"presentation-slug": self.presentation.slug, | |
"thumbnail": self.thumbnail_url(), | |
} | |
def preview_url(self): | |
# todo | |
# return "/preview/%s.png" % self.ekey | |
return f"/page/serve-image/?slide={self.id}&preview=true" | |
def thumbnail_url(self): | |
return f"/page/serve-image/?slide={self.id}&preview=false" | |
def to_public_slide_preview(self, current_slide): | |
# string ekey: | |
# string order: | |
# boolean is-current: | |
# string preview-url: | |
# slide-status status: | |
return { | |
"ekey": self.ekey, | |
"order": self.order, | |
"is-current": self.order == current_slide, | |
"preview-url": self.preview_url(), | |
"status": self.status, | |
} | |
def to_public_slide(self, ekey, presentation): | |
return { | |
"ekey": ekey, | |
"presentation": presentation.to_public_presentation(), | |
} | |
def url(self): | |
self.presentation.url() + f"/{self.order}/" | |
def __str__(self): | |
return ( | |
"Id: " | |
+ str(self.id) | |
+ " " | |
+ self.presentation.title | |
+ ": " | |
+ str(self.order) | |
) | |
class Comment(models.Model): | |
slide = models.ForeignKey(Slide, on_delete=models.PROTECT, related_name="comments") | |
user = models.ForeignKey(User, on_delete=models.PROTECT, related_name="comments") | |
content = models.TextField() | |
issue = models.BooleanField(default=False) | |
resolved = models.BooleanField(default=False) | |
position_x = models.IntegerField(null=True) | |
position_y = models.IntegerField(null=True) | |
created_at = models.DateTimeField(auto_now_add=True) | |
updated_at = models.DateTimeField(auto_now=True) | |
class Meta: | |
db_table = "slides_comment" | |
class CommentResolution(models.Model): | |
comment = models.ForeignKey( | |
Comment, on_delete=models.PROTECT, related_name="resolutions" | |
) | |
user = models.ForeignKey(User, on_delete=models.PROTECT, related_name="resolutions") | |
content = models.TextField() | |
created_at = models.DateTimeField(auto_now_add=True) | |
updated_at = models.DateTimeField(auto_now=True) | |
class Meta: | |
db_table = "slides_comment_resolution" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import django.http | |
from slides import models | |
def serve_image(request: django.http.HttpRequest): | |
# /serve-image/?slide=<id>&preview=true | |
# slide = id of the slide | |
# from . import models | |
slide_id = request.GET["slide"] | |
is_preview = request.GET["preview"] == "true" | |
slide = models.Slide.objects.get(id=slide_id) | |
snapshot = slide.snapshot | |
data = snapshot.preview if is_preview else snapshot.thumbnail | |
response = django.http.HttpResponse(data, content_type="image/png") | |
response["Content-Security-Policy"] = "frame-ancestors http://127.0.0.1:8000" | |
return response |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- import: ui.fifthtry.com/pages/slides/presentation | |
-- import: fifthtry.com/user-data | |
-- import: fifthtry.com/actions/create-slide | |
-- import: fifthtry.com/actions/delete-slide | |
-- import: fifthtry.com/actions/save-slide | |
-- import: fifthtry.com/actions/move-slide | |
-- import: fastn/processors as pr | |
-- integer order: 1 | |
$processor$: pr.request-data | |
-- string org-slug: | |
$processor$: pr.request-data | |
-- string presentation-slug: | |
$processor$: pr.request-data | |
-- presentation.page: | |
user-data: user-data | |
data: $data | |
org-slug: $org-slug | |
order: $order | |
presentation-slug: $presentation-slug | |
create-slide: create-slide | |
delete-slide: delete-slide | |
save-slide: save-slide | |
move-slide: move-slide | |
-- presentation.presentation-data data: | |
$processor$: pr.http | |
url: http://127.0.0.1:7999/view/presentation/ | |
org-slug: $org-slug | |
presentation-slug: $presentation-slug | |
order: $order |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from django.urls import path | |
from . import old_views | |
from slides import pages | |
from . import views | |
from . import actions | |
# These are slides url | |
urlpatterns = [ | |
path("action/create-blank-template/", actions.create_blank_template), | |
path("view/get-public-templates/", views.get_public_templates), | |
path("view/get-public-template/", views.get_public_template), | |
path("page/serve-image/", pages.serve_image), | |
path("action/create-presentation/", actions.CreatePresentation), | |
path("view/presentation/", views.presentation), | |
path("view/dashboard/", views.dashboard), | |
path("view/presentation-settings/", views.presentation_settings), | |
path("view/present/", views.present), | |
path("action/save-slide/", actions.SaveSlide), | |
# Creating new presentation from a template | |
# Creating new slide at the end | |
path("action/create-slide/", actions.CreateSlide), | |
# Delete slide by order | |
path("action/delete-slide/", actions.DeleteSlide), | |
# Reorder slide by direction | |
path("action/move-slide/", actions.MoveSlide), | |
path("action/toggle-template/", actions.ToggleTemplate), | |
path("action/save-presentation-settings/", actions.SavePresentationSettings), | |
path("action/change-presentation-title/", actions.ChangePresentationTitle), | |
# Present slide | |
# These are template related stuff | |
# OLD STUFF ============================================================ | |
path("template/<int:template_id>/", old_views.get_template_by_id), | |
] | |
# These are renderer url | |
urlpatterns += [ | |
path("iframe/rerender/", old_views.rerender), | |
path("iframe/<slug:folder_name>/", old_views.serve_index_file), | |
path("iframe/<slug:folder_name>/<path:filename>/", old_views.serve_file), | |
] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import django.http | |
from django.views.decorators.csrf import csrf_exempt | |
from django.views.decorators.http import require_GET | |
from django.template.defaultfilters import slugify | |
import fastn.django | |
from . import models | |
from . import utils | |
# docs: /dev/pages/presentation/ | |
# request: django.http.HttpRequest | |
# test example: | |
# http://127.0.0.1:7999/view/presentation/?org-slug=fifthtry&presentation-slug=blank-template&order=1 | |
@csrf_exempt | |
@require_GET | |
def presentation(request: django.http.HttpRequest): | |
org_slug = request.GET["org-slug"] | |
presentation_slug = request.GET["presentation-slug"] | |
order_s: str = request.GET["order"] | |
try: | |
order: int = int(order_s) | |
except (ValueError, TypeError): | |
return django.http.HttpResponseNotFound( | |
f"order parameter must be an integer value, " f"found {order_s}" | |
) | |
presentation = models.Presentation.objects.get_or_404( | |
org__slug=org_slug, slug=presentation_slug | |
) | |
current_slide = presentation.slides.get_or_404(order=order) | |
# presentation-setting-url: /slides/scenarios/fastn-presentation-logged-in/ | |
# present-url: /slides/scenarios/fastn-presentation-logged-in/ | |
# slide-templates: $slide-templates | |
# thumbnails: $slide-thumbnails | |
# todo: extract slide templates on visibility criteria | |
# based on user's privileges | |
template_slides = models.Slide.objects.filter(presentation__is_template=True) | |
slide_template_data = [s.to_template_slide_data() for s in template_slides] | |
response = { | |
"title": presentation.title, | |
"url": presentation.presentation_url(), | |
"is-valid": True, | |
"settings-url": presentation.settings_url(), | |
"present-url": presentation.present_url(), | |
"slide": current_slide.to_detail_slide(), | |
"thumbnails": [ | |
s.to_slide_thumbnail() for s in presentation.slides.all().order_by("order") | |
], | |
"slide-templates": slide_template_data, # TODO | |
"comments": [], # TODO | |
} | |
return django.http.JsonResponse(response, status=200) | |
@csrf_exempt | |
@require_GET | |
def present(request: django.http.HttpRequest): | |
presentation_slug = request.GET["presentation-slug"] | |
org_slug = request.GET["org-slug"] | |
slide_order = 1 | |
try: | |
current_presentation = models.Presentation.objects.get( | |
slug=presentation_slug, org__slug=org_slug | |
) | |
except models.Presentation.DoesNotExist: | |
return django.http.HttpResponseNotFound( | |
f"Presentation {presentation_slug} for org {org_slug} not found" | |
) | |
all_slides = models.Slide.objects.filter( | |
presentation=current_presentation | |
).order_by("order") | |
all_slide_previews = [s.preview_url() for s in all_slides] | |
response = { | |
"all-slide-previews": all_slide_previews, | |
"current-slide-order": slide_order, | |
"editor-url": f"p/{org_slug}/{presentation_slug}/", | |
} | |
return django.http.JsonResponse(response, status=200) | |
# Test | |
# http://127.0.0.1:7999/api/settings/?presentation_slug=blank-template | |
def presentation_settings(request: django.http.HttpRequest): | |
""" | |
method: get | |
expects query param: presentation_slug, org_slug | |
:return: | |
{ | |
settings-conf: string, | |
fastn-conf: string, | |
is-template: boolean, | |
editor-url: string, | |
presentation-title: string, | |
} | |
""" | |
presentation_slug = request.GET["presentation-slug"] | |
org_slug = request.GET["org-slug"] | |
try: | |
current_org = models.Org.objects.get(slug=org_slug) | |
except models.Org.DoesNotExist: | |
return django.http.HttpResponseNotFound(f"Team {org_slug} not found") | |
try: | |
current_presentation = models.Presentation.objects.get( | |
slug=presentation_slug, org__slug=org_slug | |
) | |
except models.Presentation.DoesNotExist: | |
return django.http.HttpResponseNotFound( | |
f"Presentation {presentation_slug} for org {org_slug} not found" | |
) | |
response = { | |
"presentation-title": current_presentation.title, | |
"org-name": current_org.name, | |
"fastn-conf": current_presentation.fastn_ftd, | |
"settings-conf": current_presentation.setting_ftd, | |
"is-template": current_presentation.is_template, | |
"editor-url": f"p/{org_slug}/{presentation_slug}/", | |
} | |
return django.http.JsonResponse(response, status=200) | |
def get_public_template(request: django.http.HttpRequest): | |
org_slug = request.GET["org_slug"] | |
template_slug = request.GET["template_slug"] | |
response = models.Presentation.objects.get( | |
org__slug=org_slug, slug=template_slug | |
).to_detail_presentation() | |
response["username"] = request.user.username | |
return django.http.JsonResponse(response) | |
def get_public_templates(_request: django.http.HttpRequest): | |
presentations = models.Presentation.objects.filter( | |
is_template=True, | |
visibility=models.VISIBILITY_EVERYONE, | |
) | |
template_data = [] | |
for presentation in presentations: | |
template_data.append(presentation.to_public_presentation()) | |
return django.http.JsonResponse({"templates": template_data}) | |
# dashboard expects: | |
# | |
# -- record dashboard-data: | |
# presentation list presentations: | |
# template list templates: | |
# | |
# -- record presentation: | |
# caption title: | |
# string url: | |
# string thumbnail: | |
# string updated-on: | |
# | |
# | |
# -- record template: | |
# caption title: | |
# string url: | |
# string thumbnail: | |
def dashboard(request: django.http.HttpRequest): | |
org_slug = request.GET.get("org-slug") or request.user.username | |
# org_slug = request.GET.get("org-slug", request.user.username) | |
username = request.user.username | |
current_user, _ = models.User.objects.get_or_create(username=username) | |
current_org, _ = models.Org.objects.get_or_create( | |
owner=current_user, defaults={"name": username, "slug": slugify(username)} | |
) | |
org_name = current_org.name | |
toc_links = [ | |
{"title": "Dashboard", "link": f"/t/{org_slug}/"}, | |
{"title": "Templates", "link": "/community/templates/"}, | |
] | |
# TODO: implement pagination | |
return django.http.JsonResponse( | |
{ | |
"toc-links": toc_links, | |
"org-name": org_name, | |
"org-slug": org_slug, | |
"presentations": [ | |
p.dashboard_view() | |
for p in models.Presentation.objects.filter(org__slug=org_slug)[:10] | |
], | |
} | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment