7

I'd like to see an example of a basic create-read-edit-delete application using the wolfram cloud.

For this a to-do list app would be best - where you can add a Todo item with details, and edit and remove them as well.

  • There are many ways to displaying dynamic data but all that I've tried are inefficient (takes too long to load)
  • It's unclear how to design a UI for editable data with things like FormPage and FormFunction
user5601
  • 3,573
  • 2
  • 24
  • 56

3 Answers3

3

Pardon my lack of SE-fu. I'm not sure how to publish something as a notebook, and I'm not sure what the consequences are if I publish this in my own cloud space and provide a link. So, it's just raw code in text.

Initialization

(*Initialize a cloud object to hold data.*)
ToDoList = CloudObject["ToDoList"];
CloudPut[{}, ToDoList];

To-do representation

(*I'll represent a to-do item as a structure with head ToDo. I'll
create some convenience constructors. The basic structure has a
description, a creation date, and a due date. I ended up never using
the creation date, but I had a variety of possible uses in mind.*)
NewToDo[description_String] := NewToDo[description, 1];
NewToDo[description_String, dueInDays_?NumberQ] := 
  NewToDo[description, DatePlus[Now, dueInDays]];

(Why work with associations? Well, that's the form generated by FormFunction, which I eventually used to create the web form.) NewToDo[data_Association] := NewToDo[Lookup[data, "Description", "Unspecified to-do"], Lookup[data, "DueDays", 0]]; NewToDo[description_String, dueDate_DateObject] := ToDo[description, Now, dueDate];

(Some accessors to keep things clean later on.) DueDate[todo_ToDo] := todo[[3]]; Description[todo_ToDo] := todo[[1]]; DisplayToDo[todo_ToDo] := Description[todo] <> " (" <> DateString[DueDate[todo], {"DateTimeShort"}] <> ")";

Forms for creating the to-do items

(*A simplistic method for persisting a list of to-do items.*)
SaveToDos[todoRepo_CloudObject][todos_List] := 
  CloudPut[Join[CloudGet[todoRepo], todos], todoRepo]

(The form object and form function for creating a to-do item.) ToDoCreator = CompoundElement[<|"Description" -> "String", {"DueDays", "Days until due"} -> "Number"|>]; ToDoListCreator := FormFunction[ {"ToDoList", "To do list"} -> RepeatingElement[ToDoCreator], SaveToDos[CloudObject["ToDoList"]][Map[NewToDo, #ToDoList]] &]; CloudDeploy[ToDoListCreator, "ToDoListCreator"]

Forms for completing the to-do items

(*A simplisit method that can filter out items to be deleted (marked 
as complete) based on the string description.*)
MarkComplete[todoRepo_CloudObject][descriptions : {__String}] := 
  CloudPut[
    Select[CloudGet[todoRepo], FreeQ[Alternatives @@ descriptions]], 
    todoRepo];

(Form object for displaying a to-do item, and form function for handling the (boolean) results from submitting the form.) ToDoCompleterSpec[todo_ToDo] := <|{Description[todo], DisplayToDo[todo]} -> "Boolean"|>; ToDoListWorker := FormFunction[ Join @@ ToDoCompleterSpec /@ CloudGet[CloudObject["ToDoList"]], MarkComplete[CloudObject["ToDoList"]]@Keys@DeleteCases[False]]; CloudDeploy[Delayed[ToDoListWorker], "ToDoListWorker"]

Now, if you visit the urls associated with ToDoListWorker and ToDoListCreator, you can achieve some basic functionality. If you want some sort of "all-in-one" app, you can design the forms with navigation between the edit/complete screens, or use compound form functions. I hesitate to go further without knowing if the success criteria are going to be overly complicated (or if we're going to get into a moving-the-goalpost situation).

lericr
  • 27,668
  • 1
  • 18
  • 64
  • This is cool. 1) How would you allow editing of the descriptions? 2) Is there a way to redirect after pressing submit to go back to the "home page" that displays the current list? 3) How would you allow adding attachments e.g. images per task? – user5601 Oct 06 '22 at 03:22
  • If I get time in the next couple of days, I'll try to update the answer, but rather than leave you hanging, here are some thoughts. (1) probably make the complete-to-do page use an input field for the description instead of just making it a label. And you'll need to update the handle function for the form. And thinking ahead, might want to mark to-do items as complete by setting a complete date rather than just removing them. That way everything is just one big edit. (2) Pretty sure you can do this with a multi-page FormFunction, but this needs to be confirmed. – lericr Oct 06 '22 at 04:30
  • (3) Adding images to the ToDo representation is easy, as is displaying them on the page. Not sure what controls to use for adding/editing them. – lericr Oct 06 '22 at 04:30
  • (3 continued) Do you want to fetch the images from your local computer, from another wolfram cloud resource, or some other remote web resource? (And although this is interesting, manipulating images wasn't a requirement that could have been assumed based on the description of creating a to-do app.) – lericr Oct 06 '22 at 04:55
  • 1
    No worries about images. I keep getting this “do u want to do resubmit” warning. I think the major drawback is that we can’t create/ edit/view/delete from a single page. Is that possible? – user5601 Oct 06 '22 at 13:08
2

Okay, here's an update that addresses some of the comments and demonstrates some more sophisticated behavior.

This has only been lightly tested. Also, I did no work on styling the UI. I don't know what was causing those resubmit errors before--I haven't seen anything like that with either of these versions.

(*These are just descriptions of the representations I'll use. 
Deleted ToDo items will be removed from the repository and thus will 
have no representation (you could choose to add something with a 
deleted at property).*)
Protect[ActiveToDo, CompleteToDo];
ActiveToDo[id, description, createdAt, dueAt, recurrenceFn];
CompleteToDo[id, description, createdAt, dueAt, completedAt];

(Accessors) Id[todo : (_ActiveToDo | _CompleteToDo)] := todo[[1]]; Description[todo : (_ActiveToDo | _CompleteToDo)] := todo[[2]]; CreatedAt[todo : (_ActiveToDo | _CompleteToDo)] := todo[[3]]; DueAt[todo : (_ActiveToDo | _CompleteToDo)] := todo[[4]]; RecurrenceFn[todo_ActiveToDo] := todo[[5]]; CompletedAt[todo_CompleteToDo] := todo[[5]];

(Display functions) DisplayToDo[todo_ActiveToDo] := Description[todo] <> " (" <> DateString[DueAt[todo], {"DateTimeShort"}] <> ")"; DisplayToDo[todo_CompleteToDo] := Style[Description[todo] <> " (" <> DateString[DueAt[todo], {"DateTimeShort"}] <> ")", FontVariations -> {"StrikeThrough" -> True}];

(Transformations) (I've added the idea of recurrence, so completing a recurring task results in a new active task along with the original task transformed to completed. I've provided some sample recurrence functions.) Complete[todo_CompleteToDo] := {todo}; Complete[todo_ActiveToDo] := {ReplacePart[CompleteToDo @@ todo, 5 -> Now], RecurrenceFn[todo][todo]}; RecurDaily[todo_ActiveToDo] := NewToDo[<|"Description" -> Description[todo], "DueAt" -> DatePlus[Now, 1], "Recurrence" -> RecurDaily|>]; RecurWeekly[todo_ActiveToDo] := NewToDo[<|"Description" -> Description[todo], "DueAt" -> DatePlus[Now, 7], "Recurrence" -> RecurWeekly|>]; RecurMonthly[todo_ActiveToDo] := NewToDo[<|"Description" -> Description[todo], "DueAt" -> DatePlus[Now, Quantity[1, "Months"]], "Recurrence" -> RecurMonthly|>]; RecurNever[_] := Nothing;

(Constructors) (I didn't bother with constructors that don't use an association argument, since this is intended to be used with a form.) NewToDo[] := NewToDo[<||>]; NewToDo[data_Association] := With[ {id = CreateUUID[]}, ActiveToDo[ id, Lookup[data, "Description", id], Now, Lookup[data, "DueAt", DatePlus[Now, Lookup[data, "DueDays", 1]]], Lookup[data, "Recurrence", RecurNever]]];

(Initialize a cloud object and give it some data.) ToDoList = CloudObject["ToDoList"]; CloudPut[{}, ToDoList];(use this for true initialization, the following was for testing) initData = {NewToDo[<|"Description" -> "take out garbage", "Recurrence" -> RecurWeekly|>], NewToDo[<|"Description" -> "clean filter", "Recurrence" -> RecurMonthly|>], NewToDo[<|"Description" -> "one time task", "Recurrence" -> RecurNever|>]}; CloudPut[initData, ToDoList];

(The following map allows us to provide a friendly UI for our recurrence functions.) RecurrenceMap = {"None" -> RecurNever, "Daily" -> RecurDaily, "Weekly" -> RecurWeekly, "Monthly" -> RecurMonthly};

(Simple creator widget.) ToDoCreator = CompoundElement[ <|"Description" -> "String", {"DueDays", "Days until due"} -> "Number", "Recurrence" -> Keys[RecurrenceMap]|>];

(The widget for existing tasks is a bit more complicated. Basically, we'll associate the task id with a pair of booleans that allow you to either complete or delete the task.) ToDoCompleterSpec[todo_ActiveToDo] := {{Id[todo], DisplayToDo[todo]} -> CompoundElement[{"Complete?" -> "Boolean", "Delete?" -> "Boolean"}]};

(This function is kind of messy, but suffices for a demo. The form will give us a bunch of old keys with the complete/delete choices, and it'll also have one item that contains a list of all of the new tasks. We gather all of these items into groups and process them against the existing tasks. The HTTPRedirect at the end allows us to return to the same page, making it sort of like a self-contained app.) HandleThenReturn[data_] := With[ {oldData = CloudGet[CloudObject["ToDoList"]], newItems = NewToDo /@ Lookup[data, "NewToDoList", {}] /. RecurrenceMap, completeIds = Keys[Select[data, #["Complete?"] &]], deleteIds = Keys[Select[data, #["Delete?"] &]]}, CloudPut[ Flatten[ {newItems, Map[ If[MemberQ[deleteIds, Id[#]], Nothing, If[MemberQ[completeIds, Id[#]], Complete[#], #]] &, oldData]}], ToDoList]; HTTPRedirect[CloudObject["ToDoListWorker"]]];

(And here is the form.) ToDoListWorker := FormFunction[ Flatten[ {ToDoCompleterSpec /@ Cases[CloudGet[CloudObject["ToDoList"]], _ActiveToDo], Delimiter, {"NewToDoList", "New Tasks"} -> RepeatingElement[ToDoCreator, {0, {0, Infinity}}]}], HandleThenReturn];

(Deploy the form.) CloudDeploy[Delayed[ToDoListWorker], "ToDoListWorker"]

lericr
  • 27,668
  • 1
  • 18
  • 64
  • This is nicer! 1) Is this what the ToDo list (not worker) should look like: https://imgur.com/duTcAem 2) It seems the complete checkmark doesn't do anything visibly different from delete, is that as designed? – user5601 Oct 11 '22 at 00:57
  • is there a way to show completed in gray or make the descriptions editable in place?
  • – user5601 Oct 11 '22 at 00:58
  • 1
    If you check the delete box and submit, that particular to-do will no longer be in the list of all to-do items. If you check the complete box and submit, a CompleteToDo will be created. The idea was that you might want a history of completed tasks. – lericr Oct 11 '22 at 16:10
  • As for the "editable in place", I don't know. As far as I can tell, FormFunction and related functions all expect you to actually submit the form for any action to take place. That's the way I designed this little demo. You check complete or delete as desired and then click submit. You're brought back to the same page, so it has somewhat of an interactive feel, but yeah, not quite "in place". – lericr Oct 11 '22 at 16:13
  • I've just never tried "editable in place" before, so I don't have any confident answers. Maybe you turn the checkboxes into buttons. – lericr Oct 11 '22 at 16:16