Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add queue cutoff line and queue join cooldown #14

Merged
merged 15 commits into from
Jun 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/models/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type NotificationType string

const (
NotificationClaimed NotificationType = "CLAIMED"
NotificationComplete NotificationType = "COMPLETE"
NotificationAnnouncement NotificationType = "ANNOUNCEMENT"
)

Expand Down
69 changes: 36 additions & 33 deletions internal/models/queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@ var (
)

type Queue struct {
ID string `json:"id" mapstructure:"id"`
Title string `json:"title" mapstructure:"title"`
Description string `json:"code" mapstructure:"code"`
Location string `json:"location" mapstructure:"location"`
EndTime time.Time `json:"endTime" mapstructure:"endTime"`
ShowMeetingLinks bool `json:"showMeetingLinks" mapstructure:"showMeetingLinks"`
AllowTicketEditing bool `json:"allowTicketEditing" mapstructure:"allowTicketEditing"`
CourseID string `json:"courseID" mapstructure:"courseID"`
Course *Course `json:"course" mapstructure:"course,omitempty"`
IsCutOff bool `json:"isCutOff" mapstructure:"isCutOff,omitempty"`
Tickets []string `json:"tickets" mapstructure:"tickets"`
ID string `json:"id" mapstructure:"id"`
Title string `json:"title" mapstructure:"title"`
Description string `json:"code" mapstructure:"code"`
Location string `json:"location" mapstructure:"location"`
EndTime time.Time `json:"endTime" mapstructure:"endTime"`
ShowMeetingLinks bool `json:"showMeetingLinks" mapstructure:"showMeetingLinks"`
AllowTicketEditing bool `json:"allowTicketEditing" mapstructure:"allowTicketEditing"`
CourseID string `json:"courseID" mapstructure:"courseID"`
Course *Course `json:"course" mapstructure:"course,omitempty"`
IsCutOff bool `json:"isCutOff" mapstructure:"isCutOff,omitempty"`
CutoffTicketID string `json:"cutoffTicketID" mapstructure:"cutoffTicketID,omitempty"`
Tickets []string `json:"tickets" mapstructure:"tickets"`
VisibleTickets []string `json:"visibleTickets" mapstructure:"visibleTickets"`
n-young marked this conversation as resolved.
Show resolved Hide resolved
}

type TicketStatus string
Expand All @@ -32,23 +34,24 @@ const (
)

type TicketUserdata struct {
UserID string `json:"UserID" mapstructure:"UserID"`
Email string `json:"Email" mapstructure:"Email"`
PhotoURL string `json:"PhotoURL" mapstructure:"PhotoURL"`
DisplayName string `json:"DisplayName" mapstructure:"DisplayName"`
Pronouns string `json:"Pronouns" mapstructure:"Pronouns"`
UserID string `json:"UserID" mapstructure:"UserID"`
Email string `json:"Email" mapstructure:"Email"`
PhotoURL string `json:"PhotoURL" mapstructure:"PhotoURL"`
DisplayName string `json:"DisplayName" mapstructure:"DisplayName"`
Pronouns string `json:"Pronouns" mapstructure:"Pronouns"`
}

type Ticket struct {
ID string `json:"id" mapstructure:"id"`
User TicketUserdata `json:"user" mapstructure:"user"`
Queue *Queue `json:"queue" mapstructure:"queue"`
CreatedAt time.Time `json:"createdAt" mapstructure:"createdAt"`
ClaimedAt time.Time `json:"claimedAt,omitempty" mapstructure:"claimedAt"`
ClaimedBy string `json:"claimedBy,omitempty" mapstructure:"claimedBy"`
Status TicketStatus `json:"status" mapstructure:"status"`
Description string `json:"description"`
Anonymize bool `json:"anonymize"`
ID string `json:"id" mapstructure:"id"`
User TicketUserdata `json:"user" mapstructure:"user"`
Queue *Queue `json:"queue" mapstructure:"queue"`
CreatedAt time.Time `json:"createdAt" mapstructure:"createdAt"`
ClaimedAt time.Time `json:"claimedAt,omitempty" mapstructure:"claimedAt"`
ClaimedBy string `json:"claimedBy,omitempty" mapstructure:"claimedBy"`
CompletedAt time.Time `json:"completedAt,omitempty" mapstructure:"completedAt"`
Status TicketStatus `json:"status" mapstructure:"status"`
Description string `json:"description"`
Anonymize bool `json:"anonymize"`
}

// CreateQueueRequest is the parameter struct to the CreateQueue function.
Expand Down Expand Up @@ -81,8 +84,9 @@ type DeleteQueueRequest struct {

// CutoffQueueRequest is the parameter struct to the CutoffQueue function.
type CutoffQueueRequest struct {
IsCutOff bool `json:"isCutOff"`
QueueID string `json:",omitempty"`
IsCutOff bool `json:"isCutOff"`
CutoffTicketID string `json:"cutoffTicketID"`
QueueID string `json:",omitempty"`
}

type ShuffleQueueRequest struct {
Expand All @@ -94,17 +98,17 @@ type CreateTicketRequest struct {
QueueID string `json:"queueID,omitempty"`
CreatedBy *User `json:"createdBy,omitempty"`
Description string `json:"description"`
Anonymize bool `json:"anonymize"`
Anonymize bool `json:"anonymize"`
}

// EditTicketRequest is the parameter struct to the EditTicket function.
type EditTicketRequest struct {
ID string `json:"id" mapstructure:"id"`
QueueID string `json:"queueID,omitempty"`
OwnerID string `json:"ownerID" mapstructure:"ownerID"`
OwnerID string `json:"ownerID" mapstructure:"ownerID"`
Status TicketStatus `json:"status" mapstructure:"status"`
Description string `json:"description"`
ClaimedBy *User `json:"claimedBy,omitempty"`
ClaimedBy *User `json:"claimedBy,omitempty"`
}

// DeleteTicketRequest is the parameter struct to the DeleteTicket function.
Expand All @@ -113,9 +117,8 @@ type DeleteTicketRequest struct {
QueueID string `json:"queueID,omitempty"`
}


// MakeAnnouncementRequest is the parameter struct to the MakeAnnouncement function.
type MakeAnnouncementRequest struct {
QueueID string `json:"queueID,omitempty"`
Announcement string `json:"announcement" mapstructure:"announcement"`
QueueID string `json:"queueID,omitempty"`
Announcement string `json:"announcement" mapstructure:"announcement"`
}
3 changes: 3 additions & 0 deletions internal/qerrors/qerrors.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,7 @@ var (

// Queue errors
InvalidQueueError = errors.New("the provided queue is not valid")
InvalidTicketError = errors.New("the provided ticket is not valid")
QueueCooldownError = errors.New("user already made a ticket within the last 15 minutes")
ActiveTicketError = errors.New("User already has an active ticket in queue")
)
156 changes: 125 additions & 31 deletions internal/repository/queue.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
package repository

import (
"context"
"errors"
"fmt"
"github.com/golang/glog"
"google.golang.org/api/iterator"
"log"
"math/rand"
"signmeup/internal/firebase"
"signmeup/internal/models"
"signmeup/internal/qerrors"
"time"

"github.com/golang/glog"
"google.golang.org/api/iterator"

"cloud.google.com/go/firestore"
"github.com/mitchellh/mapstructure"
)
Expand Down Expand Up @@ -47,6 +47,7 @@ func (fr *FirebaseRepository) CreateQueue(c *models.CreateQueueRequest) (queue *
"code": queue.Course.Code,
},
"tickets": []string{},
"visibleTickets": []string{},
"isCutOff": queue.IsCutOff,
"allowTicketEditing": queue.AllowTicketEditing,
"showMeetingLinks": queue.ShowMeetingLinks,
Expand Down Expand Up @@ -99,6 +100,7 @@ func (fr *FirebaseRepository) DeleteQueue(c *models.DeleteQueueRequest) error {
func (fr *FirebaseRepository) CutoffQueue(c *models.CutoffQueueRequest) error {
_, err := fr.firestoreClient.Collection(models.FirestoreQueuesCollection).Doc(c.QueueID).Update(firebase.Context, []firestore.Update{
{Path: "isCutOff", Value: c.IsCutOff},
{Path: "cutoffTicketID", Value: c.CutoffTicketID},
})
return err
}
Expand All @@ -118,6 +120,10 @@ func (fr *FirebaseRepository) ShuffleQueue(c *models.ShuffleQueueRequest) error
Path: "tickets",
Value: q.Tickets,
},
{
Path: "isCutOff",
Value: false,
},
})

if err != nil {
Expand Down Expand Up @@ -170,8 +176,12 @@ func (fr *FirebaseRepository) CreateTicket(c *models.CreateTicketRequest) (ticke
return nil, err
}

if (ticket.User.UserID == c.CreatedBy.ID) && (ticket.Status == models.StatusComplete) && (time.Now().Sub(ticket.CompletedAt).Hours() < 0.25) {
return nil, qerrors.QueueCooldownError
}

if (ticket.User.UserID == c.CreatedBy.ID) && (ticket.Status != models.StatusComplete) {
return nil, fmt.Errorf("error creating ticket: user already active in queue")
return nil, qerrors.ActiveTicketError
}
}

Expand All @@ -187,21 +197,10 @@ func (fr *FirebaseRepository) CreateTicket(c *models.CreateTicketRequest) (ticke
return nil, fmt.Errorf("error creating ticket: %v", err)
}

// Add ticket to the queue's ticket array
queueRef := fr.firestoreClient.Collection(models.FirestoreQueuesCollection).Doc(c.QueueID)
err = fr.firestoreClient.RunTransaction(firebase.Context, func(ctx context.Context, tx *firestore.Transaction) error {
doc, err := tx.Get(queueRef)
if err != nil {
return err
}
tickets, err := doc.DataAt("tickets")
if err != nil {
return err
}
updatedTickets := append(tickets.([]interface{}), ref.ID)
return tx.Set(queueRef, map[string]interface{}{
"tickets": updatedTickets,
}, firestore.MergeAll)
// Add ticket to the queue's ticket array and the queue's visible tickets array
_, err = fr.firestoreClient.Collection(models.FirestoreQueuesCollection).Doc(c.QueueID).Update(firebase.Context, []firestore.Update{
{Path: "tickets", Value: firestore.ArrayUnion(ref.ID)},
{Path: "visibleTickets", Value: firestore.ArrayUnion(ref.ID)},
})
if err != nil {
glog.Errorf("error adding ticket to queue: %v\n", err)
Expand Down Expand Up @@ -247,6 +246,66 @@ func (fr *FirebaseRepository) EditTicket(c *models.EditTicketRequest) error {
if err != nil {
glog.Warningf("error sending claim notification: %v\n", err)
}
} else if c.Status == models.StatusComplete {
updates = append(updates, firestore.Update{
Path: "completedAt",
Value: time.Now(),
})

// If this ticket is equal to CutoffTicketID on the Queue, move the CutoffTicketID to the previous ticket.
n-young marked this conversation as resolved.
Show resolved Hide resolved
if queue.CutoffTicketID == c.ID {
// Get the index of the ticket to be marked as completed.
ticketIndex := -1
for i, ticket := range queue.VisibleTickets {
if ticket == c.ID {
ticketIndex = i
break
}
}

if ticketIndex == -1 {
// This ticket is not in the queue.
return qerrors.InvalidTicketError
} else if ticketIndex == 0 {
// If this is the first ticket, set the cutoff ticket to nil.
_, err := fr.firestoreClient.Collection(models.FirestoreQueuesCollection).Doc(c.QueueID).Update(firebase.Context, []firestore.Update{
{Path: "cutoffTicketID", Value: nil},
{Path: "visibleTickets", Value: firestore.ArrayRemove(c.ID)}, // Remove the ticket from the visible tickets array.
})
if err != nil {
return err
}
} else {
// Otherwise, set the cutoff ticket to the previous ticket.
_, err := fr.firestoreClient.Collection(models.FirestoreQueuesCollection).Doc(c.QueueID).Update(firebase.Context, []firestore.Update{
{Path: "cutoffTicketID", Value: queue.VisibleTickets[ticketIndex-1]},
{Path: "visibleTickets", Value: firestore.ArrayRemove(c.ID)}, // Remove the ticket from the visible tickets array.
})
if err != nil {
return err
}
}
} else {
// Otherwise, just remove the ticket from the visible tickets array.
_, err := fr.firestoreClient.Collection(models.FirestoreQueuesCollection).Doc(c.QueueID).Update(firebase.Context, []firestore.Update{
{Path: "visibleTickets", Value: firestore.ArrayRemove(c.ID)},
})
if err != nil {
return err
}
}

// Send notification to owner
notif := models.Notification{
Title: "You've been met with!",
Body: queue.Course.Code,
Timestamp: time.Now(),
Type: models.NotificationComplete,
}
err := fr.AddNotification(c.OwnerID, notif)
if err != nil {
glog.Warningf("error sending claim notification: %v\n", err)
}
}

// Edit ticket in collection.
Expand All @@ -256,24 +315,59 @@ func (fr *FirebaseRepository) EditTicket(c *models.EditTicketRequest) error {

func (fr *FirebaseRepository) DeleteTicket(c *models.DeleteTicketRequest) error {
// Validate that this is a valid queue.
_, err := fr.GetQueue(c.QueueID)
queue, err := fr.GetQueue(c.QueueID)
if err != nil {
return qerrors.InvalidQueueError
}

// Remove ticket from tickets.
_, err = fr.firestoreClient.Collection(models.FirestoreQueuesCollection).Doc(c.QueueID).Collection(models.FirestoreTicketsCollection).Doc(c.ID).Delete(firebase.Context)
if err != nil {
return err
// If this ticket is equal to CutoffTicketID on the Queue, move the CutoffTicketID to the previous ticket.
if c.ID == queue.CutoffTicketID {
// Get the index of the ticket to be deleted.
ticketIndex := -1
for i, ticket := range queue.VisibleTickets {
if ticket == c.ID {
ticketIndex = i
break
}
}

if ticketIndex == -1 {
// This ticket is not in the queue.
return qerrors.InvalidTicketError
} else if ticketIndex == 0 {
// If this is the first ticket, set the cutoff ticket to nil.
_, err = fr.firestoreClient.Collection(models.FirestoreQueuesCollection).Doc(c.QueueID).Update(firebase.Context, []firestore.Update{
{Path: "cutoffTicketID", Value: nil},
{Path: "tickets", Value: firestore.ArrayRemove(c.ID)}, // Remove the ticket from the queue's tickets array.
{Path: "visibleTickets", Value: firestore.ArrayRemove(c.ID)},
})
if err != nil {
return err
}
} else {
// Otherwise, set the cutoff ticket to the previous ticket.
_, err = fr.firestoreClient.Collection(models.FirestoreQueuesCollection).Doc(c.QueueID).Update(firebase.Context, []firestore.Update{
{Path: "cutoffTicketID", Value: queue.VisibleTickets[ticketIndex-1]},
{Path: "tickets", Value: firestore.ArrayRemove(c.ID)}, // Remove the ticket from the queue's tickets array.
{Path: "visibleTickets", Value: firestore.ArrayRemove(c.ID)},
})
if err != nil {
return err
}
}
} else {
// Otherwise, just remove the ticket from the queue's tickets array.
_, err = fr.firestoreClient.Collection(models.FirestoreQueuesCollection).Doc(c.QueueID).Update(firebase.Context, []firestore.Update{
{Path: "tickets", Value: firestore.ArrayRemove(c.ID)},
{Path: "visibleTickets", Value: firestore.ArrayRemove(c.ID)},
})
if err != nil {
return err
}
}

// Remove ticket from queue.
_, err = fr.firestoreClient.Collection(models.FirestoreQueuesCollection).Doc(c.QueueID).Update(firebase.Context, []firestore.Update{
{
Path: "tickets",
Value: firestore.ArrayRemove(c.ID),
},
})
// Remove ticket from tickets collection.
_, err = fr.firestoreClient.Collection(models.FirestoreQueuesCollection).Doc(c.QueueID).Collection(models.FirestoreTicketsCollection).Doc(c.ID).Delete(firebase.Context)
return err
}

Expand Down