This repository has been archived by the owner on Feb 14, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 567
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #377 from daveaglick/speaker-directory
Speaker directory
- Loading branch information
Showing
48 changed files
with
1,427 additions
and
139 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
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); | ||
} | ||
} |
This file contains 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
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"); | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains 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
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)}"); | ||
} | ||
} | ||
} | ||
} |
This file contains 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
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; | ||
} | ||
} | ||
} | ||
} |
This file contains 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
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); | ||
} | ||
} | ||
} |
This file contains 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
This file contains 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
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; } | ||
} | ||
} | ||
} |
Oops, something went wrong.