Skip to content
This repository has been archived by the owner on Feb 14, 2023. It is now read-only.

Commit

Permalink
Merge pull request #377 from daveaglick/speaker-directory
Browse files Browse the repository at this point in the history
Speaker directory
  • Loading branch information
clairernovotny authored Oct 6, 2020
2 parents 0e7a95b + d627bde commit 5646f71
Show file tree
Hide file tree
Showing 48 changed files with 1,427 additions and 139 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ jobs:
dotnet-version: '3.1.x' # SDK Version to use.
- name: Build
run: dotnet run --configuration Release
env:
AzureMapsSubscriptionKey: ${{ secrets.AzureMapsSubscriptionKey }}

- name: Build And Deploy
if: github.event.pull_request.head.repo.fork != true
Expand Down
23 changes: 23 additions & 0 deletions Analyzers/SpeakerDataAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
using Statiq.Common;

namespace DotnetFoundationWeb
{
public abstract class SpeakerDataAnalyzer : SyncAnalyzer
{
public override LogLevel LogLevel { get; set; } = LogLevel.Error;

public override string[] Pipelines => new[] { nameof(Statiq.Web.Pipelines.Content) };

protected override sealed void Analyze(ImmutableArray<IDocument> documents, IAnalyzerContext context)
{
foreach (IDocument document in documents.FilterSources("community/speakers/*.md"))
{
AnalyzeSpeakerData(document, context);
}
}

protected abstract void AnalyzeSpeakerData(IDocument document, IAnalyzerContext context);
}
}
21 changes: 21 additions & 0 deletions Analyzers/ValidateSpeakerLinks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
using Statiq.Common;

namespace DotnetFoundationWeb
{
public class ValidateSpeakerLinks : SpeakerDataAnalyzer
{
protected override void AnalyzeSpeakerData(IDocument document, IAnalyzerContext context)
{
foreach (string linkKey in SpeakerLinkAttribute.GetAll().Keys)
{
if (document.ContainsKey(linkKey) && !Uri.TryCreate(document.GetString(linkKey), UriKind.Absolute, out _))
{
context.Add(document, $"{linkKey} link {document.GetString(linkKey)} is invalid");
}
}
}
}
}
79 changes: 79 additions & 0 deletions Analyzers/ValidateSpeakerTopics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Collections.Generic;
using System.Linq;
using Statiq.Common;

namespace DotnetFoundationWeb
{
public class ValidateSpeakerTopics : SpeakerDataAnalyzer
{
public static HashSet<string> Topics = new HashSet<string>
{
".NET",
"Android",
"ASP.NET",
"ASP.NET MVC",
"ASP.NET Web API",
"Architecture",
"Artificial Intelligence",
"Azure",
"Big Data",
"Blazor",
"C#",
"Containers",
"Data",
"DevOps",
"Diversity & Inclusion",
"Entity Framework",
"F#",
"Game Development",
"HoloLens",
"iOS",
"IoT",
"JavaScript",
"Machine Learning",
"macOS",
"Microsoft 365",
"Microsoft Graph",
"Microsoft Teams",
"Microservices",
"Mixed Reality",
"ML.NET",
"Mobile Development",
"NuGet",
"Open Source",
"Product Management",
"Razor",
"Security",
"Serverless",
"SignalR",
"tvOS",
"UWP",
"Visual Basic",
"Visual Studio",
"Visual Studio Code",
"Visual Studio for Mac",
"watchOS",
"Web Development",
"Windows Development",
"Windows Forms",
"WPF",
"Xamarin",
"Xamarin.Forms"
};

protected override void AnalyzeSpeakerData(IDocument document, IAnalyzerContext context)
{
IReadOnlyList<string> topics = document.GetList<string>(SiteKeys.Topics);
if (topics == null || topics.Count == 0)
{
context.Add(document, "No topics specified");
return;
}
string[] nonApprovedTopics = topics.Where(x => !Topics.Contains(x)).ToArray();
if (nonApprovedTopics.Length > 0)
{
context.Add(document, $"Document contains non-approved topic(s): {string.Join(", ", nonApprovedTopics)}");
}
}
}
}
56 changes: 56 additions & 0 deletions BlogFeedItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using Microsoft.SyndicationFeed;
using Microsoft.SyndicationFeed.Atom;
using Microsoft.SyndicationFeed.Rss;
using Statiq.Common;
using System;
using System.Collections.Generic;
using System.Linq;

namespace DotnetFoundationWeb
{
public class BlogFeedItem
{
public string Title { get; }
public string Link { get; }
public string Description { get; }
public DateTimeOffset Published { get; }
public bool Recent { get; }
public IDictionary<string, string> Links { get; }
public string Author { get; }

public BlogFeedItem(ISyndicationItem item, DateTimeOffset recent, Uri website)
{
Title = item.Title;
ISyndicationLink firstLink = item.Links.FirstOrDefault(x => x.RelationshipType == RssLinkTypes.Alternate);
if (firstLink != null)
{
Link = firstLink.Uri.IsAbsoluteUri ? firstLink.Uri.AbsoluteUri : new Uri(website, firstLink.Uri).AbsoluteUri;
}
else
{
Link = item.Id;
}

Published = item.Published != default ? item.Published : item.LastUpdated;
Recent = Published > recent;
Description = item.Description;
Links = item.Links
.Where(x => !string.IsNullOrEmpty(x.MediaType))
.GroupBy(x => x.MediaType)
.Select(x => x.First())
.ToDictionary(x => x.MediaType, x => x.Uri.ToString());

ISyndicationPerson person = item.Contributors.FirstOrDefault(x => x.RelationshipType == "author");
if (person != null)
{
Author = person.Name ?? person.Email;
}

AtomEntry atom = item as AtomEntry;
if (atom != null && !string.IsNullOrEmpty(atom.Summary))
{
Description = atom.Summary;
}
}
}
}
32 changes: 32 additions & 0 deletions FixedAtomParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Microsoft.SyndicationFeed;
using Microsoft.SyndicationFeed.Atom;
using System.Collections.Generic;
using System.Linq;

namespace DotnetFoundationWeb
{
// See https://github.com/dotnet/SyndicationFeedReaderWriter/issues/31
public class FixedAtomParser : AtomParser
{
public override IAtomEntry CreateEntry(ISyndicationContent content)
{
// Remove author and contributor entries if they don't contain an email or name
ICollection<ISyndicationContent> children = (ICollection<ISyndicationContent>)content.Fields;
ISyndicationContent author = children.FirstOrDefault(x => x.Name == AtomContributorTypes.Author);
if (author != null
&& author.Fields.FirstOrDefault(x => x.Name == "name")?.Value == null
&& author.Fields.FirstOrDefault(x => x.Name == "email")?.Value == null)
{
children.Remove(author);
}
ISyndicationContent contributor = children.FirstOrDefault(x => x.Name == AtomContributorTypes.Contributor);
if (contributor != null
&& contributor.Fields.FirstOrDefault(x => x.Name == "name")?.Value == null
&& contributor.Fields.FirstOrDefault(x => x.Name == "email")?.Value == null)
{
children.Remove(contributor);
}
return base.CreateEntry(content);
}
}
}
9 changes: 8 additions & 1 deletion Generator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,19 @@

<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.23" />
<PackageReference Include="Statiq.Web" Version="1.0.0-beta.5" />
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="Statiq.Web" Version="1.0.0-beta.7" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="4.7.0" />
</ItemGroup>

<ItemGroup>
<Folder Include="input\assets\corporate-sponsors\" />
</ItemGroup>

<!--
<ItemGroup>
<ProjectReference Include="..\..\..\statiqdev\Statiq.Web\src\Statiq.Web\Statiq.Web.csproj" />
</ItemGroup>
-->

</Project>
105 changes: 105 additions & 0 deletions GeocodeLocations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using System.Web;
using Microsoft.Extensions.Logging;
using Statiq.Common;
using Statiq.Core;

namespace DotnetFoundationWeb
{
// Looks for documents that contain "Location" metadata but not "Lat" and "Lon" and queries the Azure Maps API to add them
public class GeocodeLocations : Module
{
public static readonly JsonSerializerOptions DefaultJsonSerializerOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

// Cache responses for each location
private readonly ConcurrentDictionary<string, Task<CoordinateAbbreviated>> _coordinateCache =
new ConcurrentDictionary<string, Task<CoordinateAbbreviated>>();

private readonly Config<string> _subscriptionKey;

public GeocodeLocations(Config<string> subscriptionKey)
{
_subscriptionKey = subscriptionKey.ThrowIfNull(nameof(subscriptionKey));
}

protected override async Task<IEnumerable<IDocument>> ExecuteInputAsync(IDocument input, IExecutionContext context)
{
if (input.ContainsKey(SiteKeys.Location) && !input.ContainsKey(SiteKeys.Lat) && !input.ContainsKey(SiteKeys.Lon))
{
string location = input.GetString(SiteKeys.Location);
if (!location.IsNullOrWhiteSpace())
{
CoordinateAbbreviated coordinates = await _coordinateCache.GetOrAdd(location, async _ =>
{
context.LogInformation($"Geocoding location {location} for {input.ToSafeDisplayString()}");
string subscriptionKey = await _subscriptionKey.GetValueAsync(input, context);
if (!subscriptionKey.IsNullOrWhiteSpace())
{
using (HttpClient client = context.CreateHttpClient())
{
HttpResponseMessage responseMessage = await client.SendWithRetryAsync($"https://atlas.microsoft.com/search/address/json?&subscription-key={subscriptionKey}&api-version=1.0&language=en-US&limit=1&query={HttpUtility.UrlEncode(location)}");
if (responseMessage.IsSuccessStatusCode)
{
SearchAddressResponse searchAddressResponse;
using (Stream responseStream = await responseMessage.Content.ReadAsStreamAsync())
{
searchAddressResponse = await JsonSerializer.DeserializeAsync<SearchAddressResponse>(responseStream, DefaultJsonSerializerOptions);
}
if (searchAddressResponse.Results.Length > 0)
{
return searchAddressResponse.Results[0].Position;
}
else
{
context.LogWarning($"No results while geocoding location {location} for {input.ToSafeDisplayString()}");
}
}
else
{
context.LogWarning($"Error {responseMessage.StatusCode} while geocoding location {location} for {input.ToSafeDisplayString()}");
}
}
}
return null;
});
if (coordinates is object)
{
return input
.Clone(new MetadataItems
{
{ SiteKeys.Lat, coordinates.Lat },
{ SiteKeys.Lon, coordinates.Lon }
})
.Yield();
}
}
}
return input.Yield();
}

public class SearchAddressResponse
{
public SearchAddressResult[] Results { get; set; }
}

public class SearchAddressResult
{
public CoordinateAbbreviated Position { get; set; }
}

public class CoordinateAbbreviated
{
public double Lat { get; set; }
public double Lon { get; set; }
}
}
}
Loading

0 comments on commit 5646f71

Please sign in to comment.