Skip to content

Commit

Permalink
Co-Guide Feature (#129)
Browse files Browse the repository at this point in the history
* Data fields defined

* First working version with Co-Guides

* Co-Guide Feature

---------

Co-authored-by: Robert Brands (RiwaAdmin) <[email protected]>
  • Loading branch information
rbrands and Robert Brands (RiwaAdmin) authored Sep 16, 2023
1 parent f654310 commit b1edce7
Show file tree
Hide file tree
Showing 13 changed files with 292 additions and 8 deletions.
138 changes: 138 additions & 0 deletions MeetUpFunctions/AddCoGuide.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Web.Http;
using Aliencube.AzureFunctions.Extensions.OpenApi.Core.Attributes;
using MeetUpPlanner.Shared;
using System.Collections.Generic;

namespace MeetUpPlanner.Functions
{
public class AddCoGuide
{
private readonly ILogger _logger;
private ServerSettingsRepository _serverSettingsRepository;
private CosmosDBRepository<Participant> _cosmosRepository;
private CosmosDBRepository<CalendarItem> _calendarRepository;
public AddCoGuide(ILogger<AddParticipantToCalendarItem> logger,
ServerSettingsRepository serverSettingsRepository,
CosmosDBRepository<Participant> cosmosRepository,
CosmosDBRepository<CalendarItem> calendarRepository)
{
_logger = logger;
_serverSettingsRepository = serverSettingsRepository;
_cosmosRepository = cosmosRepository;
_calendarRepository = calendarRepository;
}

[FunctionName("AddCoGuide")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req)
{
_logger.LogInformation($"C# HTTP trigger function AddParticipantToCalendarItem processed a request.");
string tenant = req.Headers[Constants.HEADER_TENANT];
if (String.IsNullOrWhiteSpace(tenant))
{
tenant = null;
}
ServerSettings serverSettings = await _serverSettingsRepository.GetServerSettings(tenant);

string keyWord = req.Headers[Constants.HEADER_KEYWORD];
if (String.IsNullOrEmpty(keyWord) || !(serverSettings.IsUser(keyWord) || _serverSettingsRepository.IsInvitedGuest(keyWord)))
{
return new BadRequestErrorMessageResult("Keyword is missing or wrong.");
}
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
Participant participant = JsonConvert.DeserializeObject<Participant>(requestBody);
// Get and check corresponding CalendarItem
if (String.IsNullOrEmpty(participant.CalendarItemId))
{
return new OkObjectResult(new BackendResult(false, "Terminangabe fehlt."));
}
CalendarItem calendarItem = await _calendarRepository.GetItem(participant.CalendarItemId);
if (null == calendarItem)
{
return new OkObjectResult(new BackendResult(false, "Angegebenen Termin nicht gefunden."));
}
// Get participant list to check max registrations and if caller is already registered.
IEnumerable<Participant> participants = await _cosmosRepository.GetItems(p => p.CalendarItemId.Equals(calendarItem.Id));
int counter = calendarItem.WithoutHost ? 0 : 1;
int waitingCounter = 0;
int coGuideCounter = 0;
bool alreadyRegistered = false;
foreach (Participant p in participants)
{
if (p.ParticipantFirstName.Equals(participant.ParticipantFirstName) && p.ParticipantLastName.Equals(participant.ParticipantLastName))
{
// already registered
alreadyRegistered = true;
participant.Id = p.Id;
}
if (!p.IsWaiting)
{
++counter;
}
else
{
++waitingCounter;
}
if (p.IsCoGuide)
{
++coGuideCounter;
}
}
int maxRegistrationCount = calendarItem.MaxRegistrationsCount;
if (serverSettings.IsAdmin(keyWord))
{
// Admin can "overbook" a meetup to be able to add some extra guests
maxRegistrationCount *= Constants.ADMINOVERBOOKFACTOR;
}
// Add extra slots for guides
if (coGuideCounter < calendarItem.MaxCoGuidesCount)
{
maxRegistrationCount += (calendarItem.MaxCoGuidesCount - coGuideCounter);
}
if (!alreadyRegistered)
{
if (counter < maxRegistrationCount)
{
++counter;
participant.IsWaiting = false;
}
else if (waitingCounter < calendarItem.MaxWaitingList)
{
++waitingCounter;
participant.IsWaiting = true;
}
else
{
return new OkObjectResult(new BackendResult(false, "Maximale Anzahl Registrierungen bereits erreicht."));
}
}
// Set TTL for participant the same as for CalendarItem
System.TimeSpan diffTime = calendarItem.StartDate.Subtract(DateTime.Now);
participant.TimeToLive = serverSettings.AutoDeleteAfterDays * 24 * 3600 + (int)diffTime.TotalSeconds;
// Checkindate to track bookings
participant.CheckInDate = DateTime.Now;
// Set CoGuide flag
participant.IsCoGuide = true;
if (null != tenant)
{
participant.Tenant = tenant;
}
participant.Federation = serverSettings.Federation;

participant = await _cosmosRepository.UpsertItem(participant);
BackendResult result = new BackendResult(true);

return new OkObjectResult(result);

}
}
}
2 changes: 1 addition & 1 deletion MeetUpFunctions/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public static class Constants
public const string DEFAULT_DISCLAIMER = "Disclaimer";
public const string DEFAULT_GUEST_DISCLAIMER = "Guest Disclaimer";

public const string VERSION = "2023-09-15";
public const string VERSION = "2023-09-16";
public const int ADMINOVERBOOKFACTOR = 1; // no overbooking any more, because not needed

public const int LOG_TTL = 30 * 24 * 3600; // 30 days TTL for Log items
Expand Down
2 changes: 1 addition & 1 deletion MeetUpFunctions/MeetUpPlanner.Functions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<ItemGroup>
<PackageReference Include="Aliencube.AzureFunctions.Extensions.OpenApi.Core" Version="3.1.1" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.18.0" />
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.35.3" />
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.35.4" />
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="5.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" />
Expand Down
2 changes: 1 addition & 1 deletion MeetUpPlanner/Client/Pages/About.razor
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
</div>

@code {
private const string clientVersion = "2023-09-15";
private const string clientVersion = "2023-09-16";
private string serverVersion = "tbd";
private string functionsVersion = "tbd";

Expand Down
55 changes: 55 additions & 0 deletions MeetUpPlanner/Client/Pages/Calendar.razor
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@
@if (!CheckIfUserIsHostWithoutKeyword(item) && !CheckIfUserIsHost(item) && !ShowHistory())
{
<button class="btn btn-sm btn-outline-secondary" title="@GetCheckInLabel(item)" disabled="@checkInDisabled" hidden="@(CheckIfUserIsAlreadyRegistered(item) || item.IsCanceled)" @onclick="@(() => Checkin(item.Id))">@GetCheckInLabel(item)</button>
<button class="btn btn-sm btn-outline-secondary" title="@GetCheckInAsCoGuideLabel(item)" disabled="@checkInDisabled" hidden="@(!CheckIfCoGuideIsWanted(item) || item.IsCanceled)" @onclick="@(() => CheckinAsCoGuide(item.Id))">@GetCheckInAsCoGuideLabel(item)</button>
<button class="btn btn-sm btn-outline-secondary" title="Abmelden" hidden="@(!CheckIfUserIsAlreadyRegistered(item))" @onclick="@(() => Checkout(item))">Abmelden</button>
}
</div>
Expand Down Expand Up @@ -467,6 +468,51 @@
}
checkInDisabled = false;
}
protected async Task CheckinAsCoGuide(string itemId)
{
Participant participant = new Participant();
participant.ParticipantFirstName = AppStateStore.FirstName;
participant.ParticipantLastName = AppStateStore.LastName;
participant.ParticipantAdressInfo = AppStateStore.PhoneMail;
participant.CalendarItemId = itemId;
participant.IsCoGuide = true;
checkInDisabled = true;
StateHasChanged();
PrepareHttpClient();
HttpResponseMessage response = await Http.PostAsJsonAsync<Participant>($"Calendar/addparticipantascoguide", participant);
string responseBody = await response.Content.ReadAsStringAsync();

BackendResult result = JsonConvert.DeserializeObject<BackendResult>(responseBody);
if (result.Success)
{
if (IsConnected) await SendMessage();
// Read data again
await ReadData();
foreach (ExtendedCalendarItem c in calendarItems)
{
if (c.Id.Equals(itemId))
{
participant = c.FindParticipant(participant.ParticipantFirstName, participant.ParticipantLastName);
break;
}
}
if (null != participant && participant.IsWaiting)
{
notificationService.Notify(new NotificationMessage() { Severity = NotificationSeverity.Warning, Summary = "Warteliste", Detail = "Du stehst jetzt auf der Warteliste. Falls du doch nicht dabei sein kannst, melde dich bitte wieder ab.", Duration = 4000 });
}
else
{
notificationService.Notify(new NotificationMessage() { Severity = NotificationSeverity.Success, Summary = "Angemeldet", Detail = "Du bist jetzt angemeldet. Falls du doch nicht dabei sein kannst, melde dich bitte wieder ab.", Duration = 4000 });
}
// Read data again
await ReadData();
}
else
{
notificationService.Notify(new NotificationMessage() { Severity = NotificationSeverity.Error, Summary = "Fehler", Detail = result.Message, Duration = 4000 });
}
checkInDisabled = false;
}
protected async Task Checkout(ExtendedCalendarItem calendarItem)
{
// Find corresponding participant item
Expand All @@ -492,6 +538,10 @@
bool alreadyRegistered = calendarItem.FindParticipant(AppStateStore.FirstName, AppStateStore.LastName) != null;
return alreadyRegistered;
}
protected bool CheckIfCoGuideIsWanted(ExtendedCalendarItem calendarItem)
{
return (KeywordCheck.IsUser && calendarItem.MaxCoGuidesCount > 0);
}
protected bool CheckIfUserIsHost(ExtendedCalendarItem calendarItem)
{
bool isHost = !calendarItem.WithoutHost && KeywordCheck.IsUser && (calendarItem.HostFirstName.Equals(AppStateStore.FirstName) && calendarItem.HostLastName.Equals(AppStateStore.LastName));
Expand Down Expand Up @@ -608,6 +658,11 @@
}
return checkInLabel;
}
private string GetCheckInAsCoGuideLabel(ExtendedCalendarItem calendarItem)
{
string checkInLabel = "als Co-Guide";
return checkInLabel;
}
private string GetScopeLink(ExtendedCalendarItem calendarItem)
{
return $"{Http.BaseAddress}{calendarItem.GuestScope}";
Expand Down
12 changes: 12 additions & 0 deletions MeetUpPlanner/Client/Pages/NewMeetUp.razor
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,18 @@
Optional: Gewünschte Mindestteilnehmerzahl der Gruppe. Dies wird entsprechend angezeigt, hat aber ansonsten keine Konsequenzen, d.h. es wird kein Termin automatisch abgesagt oder gelöscht ...
</small>
</div>
<div class="form-group">
<label for="CoGuides">Anzahl gewünschte Co-Guides</label>
<InputSelect id="CoGuides" @bind-Value="meetup.MaxCoGuidesCount" class="form-control">
<option value="0">Kein Co-Guide</option>
<option value="1">Ein Co-Guide gewünscht</option>
<option value="2">Zwei Co-Guides gewünscht</option>
<option value="3">Drei Co-Guides gewünscht</option>
</InputSelect>
<small id="standardPlaceHelp" class="form-text text-muted">
Wünscht du dir Unterstützung von Co-Guides? Du kannst bis zu drei Co-Guides anfragen.
</small>
</div>
<div class="form-group">
<label for="privateKeyword">Private Ausfahrt?</label>
<InputText id="privateKeyword" aria-describedby="privateKeywordHelp" class="form-control" @bind-Value="meetup.PrivateKeyword" autocomplete="on" placeholder="Schlüsselwort für private Ausfahrt" />
Expand Down
14 changes: 10 additions & 4 deletions MeetUpPlanner/Client/Shared/ParticipantsList.razor
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@
<button class="btn btn-sm btn-outline-secondary" disabled="@saveDisabled" @onclick="@(() => ConfirmDelete(p))"><span class="oi oi-trash" title="Abmelden"></span></button>
@if (p.Federation == AppStateStore.Tenant.FederationKey)
{
@p.ParticipantDisplayName(NameDisplayLength)<text> - </text>@p.ParticipantAdressInfo
@p.ParticipantDisplayNameWithCoGuideSuffix(NameDisplayLength)<text> - </text>@p.ParticipantAdressInfo
}
else
{
@p.ParticipantDisplayName(NameDisplayLength)<span class="badge badge-success">@p.Federation</span><text> - </text>@p.ParticipantAdressInfo
@p.ParticipantDisplayNameWithCoGuideSuffix(NameDisplayLength)<span class="badge badge-success">@p.Federation</span><text> - </text>@p.ParticipantAdressInfo
}
</li>
}
Expand All @@ -40,11 +40,17 @@
<button class="btn btn-sm btn-outline-secondary" disabled="@saveDisabled" @onclick="@(() => ConfirmDelete(p))"><span class="oi oi-trash" title="Abmelden"></span></button>
@if (p.Federation == AppStateStore.Tenant.FederationKey)
{
@p.ParticipantDisplayName(NameDisplayLength)<text> - </text>@p.ParticipantAdressInfo
@p.ParticipantDisplayNameWithCoGuideSuffix(NameDisplayLength)<text> - </text>@p.ParticipantAdressInfo
}
else
{
@p.ParticipantDisplayName(NameDisplayLength)<span class="badge badge-success">@p.Federation</span><text> - </text>@p.ParticipantAdressInfo
@p.ParticipantDisplayNameWithCoGuideSuffix(NameDisplayLength)

<span class="badge badge-success">@p.Federation</span>

<text> - </text>

@p.ParticipantAdressInfo
}
</li>
}
Expand Down
11 changes: 11 additions & 0 deletions MeetUpPlanner/Server/Controllers/CalendarController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,17 @@ public async Task<IActionResult> AddParticipant([FromHeader(Name = "x-meetup-ten
BackendResult result = await _meetUpFunctions.AddParticipantToCalendarItem(tenant, keyword, participant);
return Ok(result);
}
[HttpPost("addparticipantascoguide")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> AddParticipantAsCoGuide([FromHeader(Name = "x-meetup-tenant")] string tenant, [FromHeader(Name = "x-meetup-keyword")] string keyword, [FromBody] Participant participant)
{
if (String.IsNullOrEmpty(keyword))
{
keyword = _meetUpFunctions.InviteGuestKey;
}
BackendResult result = await _meetUpFunctions.AddParticipantAsCoGuideToCalendarItem(tenant, keyword, participant);
return Ok(result);
}
[HttpPost("addguest")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> AddGuest([FromHeader(Name = "x-meetup-tenant")] string tenant, [FromBody] Participant participant)
Expand Down
2 changes: 1 addition & 1 deletion MeetUpPlanner/Server/Controllers/UtilController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class UtilController : ControllerBase
{
private readonly MeetUpFunctions _meetUpFunctions;
private readonly ILogger<UtilController> logger;
const string serverVersion = "2023-09-15";
const string serverVersion = "2023-09-16";
string functionsVersion = "tbd";

public UtilController(ILogger<UtilController> logger, MeetUpFunctions meetUpFunctions)
Expand Down
10 changes: 10 additions & 0 deletions MeetUpPlanner/Server/Repositories/MeetUpFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,16 @@ public async Task<BackendResult> AddParticipantToCalendarItem(string tenant, str
.ReceiveJson<BackendResult>();
return result;
}
public async Task<BackendResult> AddParticipantAsCoGuideToCalendarItem(string tenant, string keyword, Participant participant)
{
BackendResult result = await $"https://{_functionsConfig.FunctionAppName}.azurewebsites.net/api/AddCoGuide"
.WithHeader(HEADER_FUNCTIONS_KEY, _functionsConfig.ApiKey)
.WithHeader(HEADER_KEYWORD, keyword)
.WithHeader(HEADER_TENANT, tenant)
.PostJsonAsync(participant)
.ReceiveJson<BackendResult>();
return result;
}
public async Task<BackendResult> AddCommentToCalendarItem(string tenant, string keyword, CalendarComment comment)
{
BackendResult result;
Expand Down
2 changes: 2 additions & 0 deletions MeetUpPlanner/Shared/CalendarItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public class CalendarItem : CosmosDBEntity
public int MinRegistrationsCount { get; set; } = 0;
[JsonProperty(PropertyName = "maxWaitingList", NullValueHandling = NullValueHandling.Ignore), Range(0.0, 150.0, ErrorMessage = "Größe der Warteliste nicht im gültigen Bereich."), Display(Name = "Maximale Anzahl auf Warteliste", Prompt = "Anzahl eingeben"), Required(ErrorMessage = "Max. Anzahl auf Warteliste eingeben")]
public int MaxWaitingList { get; set; } = 0;
[JsonProperty(PropertyName = "maxCoGuidesCount", NullValueHandling = NullValueHandling.Ignore)]
public int MaxCoGuidesCount { get; set; } = 0;
[JsonProperty(PropertyName = "privateKeyword", NullValueHandling = NullValueHandling.Ignore), MaxLength(50, ErrorMessage = "Privates Schlüsselwort zu lang.")]
public string PrivateKeyword { get; set; }
[JsonProperty(PropertyName = "isInternal")]
Expand Down
Loading

0 comments on commit b1edce7

Please sign in to comment.