Improved client handling and episode models

This commit is contained in:
2020-02-27 23:11:36 +01:00
parent 82722e77d6
commit 2d2f8b6b76
22 changed files with 540 additions and 257 deletions

View File

@@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using EstusShots.Shared.Dto;
using EstusShots.Shared.Models; using EstusShots.Shared.Models;
namespace EstusShots.Client namespace EstusShots.Client
@@ -23,12 +25,39 @@ namespace EstusShots.Client
HttpClient = new HttpClient {Timeout = TimeSpan.FromSeconds(10)}; HttpClient = new HttpClient {Timeout = TimeSpan.FromSeconds(10)};
} }
public async Task<List<Season>> GetSeasons() public async Task<(OperationResult, List<Season>)> GetSeasons()
{ {
var response = HttpClient.GetAsync(ApiUrl + "seasons").Result; try
{
var response = await HttpClient.GetAsync(ApiUrl + "seasons");
var jsonData = await response.Content.ReadAsStringAsync(); var jsonData = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<List<Season>>(jsonData, _serializerOptions); var data = JsonSerializer.Deserialize<List<Season>>(jsonData, _serializerOptions);
return data; return (new OperationResult(), data);
} }
catch (Exception e)
{
return (new OperationResult(e), new List<Season>());
}
}
public async Task<(OperationResult, Guid)> CreateSeason(Season season)
{
try
{
var content = new StringContent(JsonSerializer.Serialize(season), Encoding.UTF8, "application/json");
var response = await HttpClient.PostAsync(ApiUrl + "season", content);
if (!response.IsSuccessStatusCode)
{
return (new OperationResult(false, response.ReasonPhrase), Guid.Empty);
}
// TODO should give the created id
return (new OperationResult(), Guid.Empty);
}
catch (Exception e)
{
return (new OperationResult(e), Guid.Empty);
}
}
} }
} }

View File

@@ -0,0 +1,168 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GLib;
using Gtk;
using DateTime = GLib.DateTime;
namespace EstusShots.Gtk.Controls
{
public class SelectionChangedEventArgs : EventArgs
{
public SelectionChangedEventArgs(object selection)
{
Selection = selection;
}
public object Selection { get; }
}
public delegate void SelectionChangedEventHandler(object o, SelectionChangedEventArgs args);
public class BindableListControl<T>
{
/// <summary>
/// Initialize a new BindableListView with an existing TreeView Widget
/// </summary>
/// <param name="columns">The columns of the grid</param>
/// <param name="keyField">Unique key field in the item sources type</param>
/// <param name="treeView">An instance of an existing TreeView Widget</param>
public BindableListControl(List<DataColumn> columns, string keyField, TreeView treeView = null)
{
TreeView = treeView ?? new TreeView();
Columns = columns;
KeyField = keyField;
InitTreeViewColumns();
InitListStore();
TreeView.Model = ListStore;
Items = new List<T>();
TreeView.Selection.Changed += TreeView_SelectionChanged;
}
/// <summary> The GTK ListStore that is managed by this <see cref="BindableListControl{T}" />. </summary>
public ListStore ListStore { get; internal set; }
/// <summary> The GTK TreeView control that is managed by this <see cref="BindableListControl{T}" />. </summary>
public TreeView TreeView { get; }
/// <summary> Property of the element type that is used as a unique identifier for accessing elements. </summary>
public string KeyField { get; }
/// <summary> The collection of all elements, that should be shown in the list view. </summary>
public List<T> Items { get; set; }
/// <summary> The currently selected item in the view. </summary>
public T SelectedItem { get; set; }
/// <summary> All columns that are displayed in the list. </summary>
public List<DataColumn> Columns { get; }
/// <summary>
/// Event will be invoked when the selected item in the <see cref="TreeView" /> has changed.
/// </summary>
public event SelectionChangedEventHandler OnSelectionChanged;
/// <summary>
/// Set elements from the <see cref="Items" /> property in the <see cref="ListStore" />.
/// </summary>
/// <exception cref="TypeLoadException"></exception>
public void DataBind()
{
ListStore.Clear();
Items.ForEach(BindItem);
}
private void BindItem(T item)
{
var row = new List<object>();
foreach (var column in Columns)
{
var prop = item.GetType().GetProperty(column.PropertyName);
if (prop == null)
throw new TypeLoadException(
$"Property '{column.PropertyName}' does not exist on Type '{item.GetType()}'");
var val = prop.GetValue(item);
if (column.Format != null) val = column.Format(val);
row.Add(val.ToString());
}
// The key value must be the first value in the row
var key = item.GetType().GetProperty(KeyField)?.GetValue(item);
row.Insert(0, key);
try
{
ListStore.AppendValues(row.ToArray());
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
private void TreeView_SelectionChanged(object sender, EventArgs e)
{
if (!(sender is TreeSelection selection)) return;
selection.GetSelected(out var model, out var iter);
var key = model.GetValue(iter, 0);
var item = Items.FirstOrDefault(x =>
{
var prop = x.GetType().GetProperty(KeyField);
var value = prop?.GetValue(x);
return value != null && value.Equals(key);
});
if (item == null)
{
Console.WriteLine($"No item for key '{key}' found in data store");
return;
}
SelectedItem = item;
OnSelectionChanged?.Invoke(this, new SelectionChangedEventArgs(SelectedItem));
}
private void InitTreeViewColumns()
{
foreach (var dataColumn in Columns)
{
// Offset by one, because the first column in the data store is fixed to the key value of the row
var index = Columns.IndexOf(dataColumn) + 1;
var column = new TreeViewColumn(
dataColumn.Title,
new CellRendererText(),
"text", index)
{
Resizable = true,
Reorderable = true
};
TreeView.AppendColumn(column);
}
}
private void InitListStore()
{
var types = Columns
.Select(x =>
{
var propType = typeof(T).GetProperty(x.PropertyName)?.PropertyType;
var gType = (GType) propType;
if (gType.ToString() == "GtkSharpValue") gType = MapType(propType);
return gType;
});
var data = new DateTime();
// The first column in the data store is always the key field.
var columns = new List<GType> {(GType) typeof(T).GetProperty(KeyField)?.PropertyType};
columns.AddRange(types);
ListStore = new ListStore(columns.ToArray());
}
private static GType MapType(Type type)
{
return type.Name switch
{
_ => GType.String
};
}
}
}

View File

@@ -1,135 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Gtk;
namespace EstusShots.Gtk.Controls
{
public class SelectionChangedEventArgs : EventArgs
{
public object Selection { get; }
public SelectionChangedEventArgs(object selection)
{
Selection = selection;
}
}
public delegate void SelectionChangedEventHandler(object o, SelectionChangedEventArgs args);
public class BindableListView<T>
{
public ListStore ListStore { get; internal set; }
public TreeView TreeView { get; }
public string KeyField { get; }
public IEnumerable<T> Items { get; set; }
public T SelectedItem { get; set; }
public List<DataColumn> Columns { get; }
public event SelectionChangedEventHandler OnSelectionChanged;
/// <summary>
/// Initialize a new BindableListView with an existing TreeView Widget
/// </summary>
/// <param name="columns">The columns of the grid</param>
/// <param name="keyField">Unique key field in the item sources type</param>
/// <param name="treeView">An instance of an existing TreeView Widget</param>
public BindableListView(List<DataColumn> columns, string keyField, TreeView treeView = null)
{
TreeView = treeView ?? new TreeView();
Columns = columns;
KeyField = keyField;
InitTreeViewColumns();
InitListStore();
TreeView.Model = ListStore;
Items = new List<T>();
TreeView.Selection.Changed += TreeView_SelectionChanged;
}
private void TreeView_SelectionChanged(object sender, EventArgs e)
{
if (!(sender is TreeSelection selection)) return;
selection.GetSelected(out var model, out var iter);
var key = model.GetValue(iter, 0);
var item = Items.FirstOrDefault(x =>
{
var prop = x.GetType().GetProperty(KeyField);
var value = prop?.GetValue(x);
return value != null && value.Equals(key);
});
if (item == null)
{
Console.WriteLine($"No item for key '{key}' found in data store");
return;
}
SelectedItem = item;
OnSelectionChanged?.Invoke(this, new SelectionChangedEventArgs(SelectedItem));
}
public void DataBind()
{
ListStore.Clear();
foreach (var item in Items)
{
var row = new List<object>();
foreach (var column in Columns)
{
var prop = item.GetType().GetProperty(column.PropertyName);
if (prop == null)
throw new TypeLoadException(
$"Property '{column.PropertyName}' does not exist on Type '{item.GetType()}'");
row.Add(prop.GetValue(item));
}
// The key value must be the first value in the row
var key = item.GetType().GetProperty(KeyField)?.GetValue(item);
row.Insert(0, key);
try
{
ListStore.AppendValues(row.ToArray());
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
private void InitTreeViewColumns()
{
foreach (var dataColumn in Columns)
{
// Offset by one, because the first column in the data store is fixed to the key value of the row
var index = Columns.IndexOf(dataColumn) + 1;
var column = new TreeViewColumn(
dataColumn.Title,
new CellRendererText(),
"text", index);
TreeView.AppendColumn(column);
}
}
private void InitListStore()
{
var types = Columns
.Select(x => typeof(T).GetProperty(x.PropertyName)?.PropertyType);
// The first column in the data store is always the key field.
var columns = new List<Type> {typeof(T).GetProperty(KeyField)?.PropertyType};
columns.AddRange(types);
ListStore = new ListStore(columns.ToArray());
}
}
}

View File

@@ -1,16 +1,32 @@
using System;
namespace EstusShots.Gtk.Controls namespace EstusShots.Gtk.Controls
{ {
public class DataColumn public class DataColumn
{ {
public string PropertyName { get; set; } public DataColumn()
{
public string Title { get; set; } }
public DataColumn() { }
public DataColumn(string propertyName) public DataColumn(string propertyName)
{ {
PropertyName = propertyName; PropertyName = propertyName;
} }
/// <summary>
/// The name of the property in the data source, that should be show nin the view
/// </summary>
public string PropertyName { get; }
/// <summary>
/// The column header.
/// </summary>
public string Title { get; set; }
/// <summary>
/// Applies the given transformation on each item in the column.
/// This changes only the display of the value.
/// </summary>
public Func<object, string> Format { get; set; }
} }
} }

View File

@@ -0,0 +1,26 @@
using System;
namespace EstusShots.Gtk.Controls
{
internal class LoadingMode : IDisposable
{
private MainWindow Window { get; set; }
public LoadingMode(MainWindow window)
{
Window = window;
Window.LoadButton.Sensitive = false;
Window.NewSeasonButton.Sensitive = false;
Window.SeasonsView.Sensitive = false;
Window.SeasonsOverlay.AddOverlay(Window.LoadingSpinner);
}
public void Dispose()
{
Window.LoadButton.Sensitive = true;
Window.NewSeasonButton.Sensitive = true;
Window.SeasonsView.Sensitive = true;
Window.SeasonsOverlay.Remove(Window.LoadingSpinner);
}
}
}

View File

@@ -21,8 +21,4 @@
<ProjectReference Include="..\EstusShots.Shared\EstusShots.Shared.csproj" /> <ProjectReference Include="..\EstusShots.Shared\EstusShots.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="DataStores" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,35 +1,28 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Threading.Tasks;
using System.Text;
using System.Text.Json;
using EstusShots.Client; using EstusShots.Client;
using EstusShots.Gtk.Controls; using EstusShots.Gtk.Controls;
using EstusShots.Shared.Models; using EstusShots.Shared.Dto;
using Gtk; using Gtk;
using Application = Gtk.Application;
using DateTime = System.DateTime;
using Task = System.Threading.Tasks.Task;
using UI = Gtk.Builder.ObjectAttribute; using UI = Gtk.Builder.ObjectAttribute;
namespace EstusShots.Gtk namespace EstusShots.Gtk
{ {
class MainWindow : Window internal class MainWindow : Window
{ {
private const string ApiUrl = "http://localhost:5000/api/"; private const string ApiUrl = "http://localhost:5000/api/";
private EstusShotsClient Client { get; }
private BindableListView<Season> SeasonsView { get; }
[UI] private readonly TreeView _seasonsView = null;
[UI] private readonly Button _loadButton = null;
[UI] private readonly Button _newSeasonButton = null;
[UI] private readonly Label _infoLabel = null; [UI] private readonly Label _infoLabel = null;
[UI] private readonly Overlay _seasonsOverlay = null; [UI] public readonly Button LoadButton = null;
[UI] private readonly Box _loadingSpinner = null; [UI] public readonly Box LoadingSpinner = null;
[UI] public readonly Button NewSeasonButton = null;
[UI] public readonly Overlay SeasonsOverlay = null;
[UI] public readonly TreeView SeasonsView = null;
public MainWindow() : this(new Builder("MainWindow.glade")) { } public MainWindow() : this(new Builder("MainWindow.glade"))
{
}
private MainWindow(Builder builder) : base(builder.GetObject("MainWindow").Handle) private MainWindow(Builder builder) : base(builder.GetObject("MainWindow").Handle)
{ {
@@ -37,17 +30,29 @@ namespace EstusShots.Gtk
Client = new EstusShotsClient("http://localhost:5000/api/"); Client = new EstusShotsClient("http://localhost:5000/api/");
DeleteEvent += Window_DeleteEvent; DeleteEvent += Window_DeleteEvent;
_loadButton.Clicked += LoadButtonClicked; LoadButton.Clicked += LoadButtonClicked;
_newSeasonButton.Clicked += NewSeasonButtonOnClicked; NewSeasonButton.Clicked += NewSeasonButtonOnClicked;
var seasonsColumns = new List<DataColumn> var seasonsColumns = new List<DataColumn>
{ {
new DataColumn(nameof(Season.DisplayName)){Title = "Name"} new DataColumn(nameof(Season.DisplayName)) {Title = "Name"},
}; new DataColumn(nameof(Season.Description)) {Title = "Description"},
SeasonsView = new BindableListView<Season>(seasonsColumns, nameof(Season.SeasonId) ,_seasonsView); new DataColumn(nameof(Season.Start))
SeasonsView.OnSelectionChanged += SeasonsViewOnOnSelectionChanged; {
Info("Application Started"); Title = "Start",
Format = date => (date as DateTime?)?.ToString("dd.MM.yyyy hh:mm")
} }
};
SeasonsControl = new BindableListControl<Season>(seasonsColumns, nameof(Season.SeasonId), SeasonsView);
SeasonsControl.OnSelectionChanged += SeasonsViewOnOnSelectionChanged;
Info("Application Started");
// No need to wait for the loading to finnish
var _ = ReloadSeasons();
}
private EstusShotsClient Client { get; }
private BindableListControl<Season> SeasonsControl { get; }
private void SeasonsViewOnOnSelectionChanged(object sender, SelectionChangedEventArgs e) private void SeasonsViewOnOnSelectionChanged(object sender, SelectionChangedEventArgs e)
{ {
@@ -57,49 +62,45 @@ namespace EstusShots.Gtk
private async void NewSeasonButtonOnClicked(object sender, EventArgs e) private async void NewSeasonButtonOnClicked(object sender, EventArgs e)
{ {
var nextNum = SeasonsView.Items.Any() ? SeasonsView.Items.Max(x => x.Number) + 1 : 1 ; using var _ = new LoadingMode(this);
var season = new Season var season = new Season
{ {
Game = "Test Game", Game = "Test Game",
Number = nextNum, Number = SeasonsControl.Items.Any() ? SeasonsControl.Items.Max(x => x.Number) + 1 : 1,
Start = DateTime.Now Start = DateTime.Now,
Description = "This is a demo description!"
}; };
var content = new StringContent(JsonSerializer.Serialize(season), Encoding.UTF8, "application/json");
var client = new HttpClient();
try
{
var response = await client.PostAsync(ApiUrl + "season", content);
if (!response.IsSuccessStatusCode) var (res, _) = await Client.CreateSeason(season);
if (!res.Success)
{ {
_infoLabel.Text = $"Error while creating Season: {response.ReasonPhrase}"; _infoLabel.Text = $"Error while creating Season: {res.ShortMessage}";
return; return;
} }
await ReloadSeasons(); await ReloadSeasons();
Info("Created new Season"); Info("Created new Season");
} }
catch (Exception ex)
{
_infoLabel.Text = $"Exception Occured: {ex.Message}";
Console.WriteLine(ex.Message);
}
}
private async void LoadButtonClicked(object sender, EventArgs a) private async void LoadButtonClicked(object sender, EventArgs a)
{ {
using var _ = new LoadingMode(this);
Info("Loading Seasons..."); Info("Loading Seasons...");
await ReloadSeasons(); await ReloadSeasons();
Info("List Refreshed");
} }
private async Task ReloadSeasons() private async Task ReloadSeasons()
{ {
LoadingMode(true); var (res, seasons) = await Task.Factory.StartNew(() => Client.GetSeasons().Result);
var seasons = await Task.Factory.StartNew(() => Client.GetSeasons().Result); if (!res.Success)
SeasonsView.Items = seasons; {
SeasonsView.DataBind(); _infoLabel.Text = $"Refresh Failed: {res.ShortMessage}";
LoadingMode(false); return;
}
SeasonsControl.Items = seasons;
SeasonsControl.DataBind();
Info("List Refreshed");
} }
private void Window_DeleteEvent(object sender, DeleteEventArgs a) private void Window_DeleteEvent(object sender, DeleteEventArgs a)
@@ -107,20 +108,10 @@ namespace EstusShots.Gtk
Application.Quit(); Application.Quit();
} }
private void LoadingMode(bool active)
{
_loadButton.Sensitive = !active;
_newSeasonButton.Sensitive = !active;
_seasonsView.Sensitive = !active;
if (active)
_seasonsOverlay.AddOverlay(_loadingSpinner);
else
_seasonsOverlay.Remove(_loadingSpinner);
}
private void Info(string message) private void Info(string message)
{ {
_infoLabel.Text = message; _infoLabel.Text = message;
} }
} }
} }

View File

@@ -2,6 +2,37 @@
<!-- Generated with glade 3.22.1 --> <!-- Generated with glade 3.22.1 -->
<interface> <interface>
<requires lib="gtk+" version="3.18"/> <requires lib="gtk+" version="3.18"/>
<object class="GtkBox" id="LoadingSpinner">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="spacing">5</property>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Loading....</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<object class="GtkWindow" id="MainWindow"> <object class="GtkWindow" id="MainWindow">
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="title" translatable="yes">Estus Shots</property> <property name="title" translatable="yes">Estus Shots</property>
@@ -20,7 +51,7 @@
<property name="margin_bottom">2</property> <property name="margin_bottom">2</property>
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<child> <child>
<object class="GtkOverlay" id="_seasonsOverlay"> <object class="GtkOverlay" id="SeasonsOverlay">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<child> <child>
@@ -29,7 +60,7 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="shadow_type">in</property> <property name="shadow_type">in</property>
<child> <child>
<object class="GtkTreeView" id="_seasonsView"> <object class="GtkTreeView" id="SeasonsView">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="enable_grid_lines">horizontal</property> <property name="enable_grid_lines">horizontal</property>
@@ -56,7 +87,7 @@
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="layout_style">end</property> <property name="layout_style">end</property>
<child> <child>
<object class="GtkButton" id="_loadButton"> <object class="GtkButton" id="LoadButton">
<property name="label" translatable="yes">Reload</property> <property name="label" translatable="yes">Reload</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
@@ -69,7 +100,7 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkButton" id="_newSeasonButton"> <object class="GtkButton" id="NewSeasonButton">
<property name="label" translatable="yes">New Season</property> <property name="label" translatable="yes">New Season</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
@@ -132,35 +163,4 @@
</object> </object>
</child> </child>
</object> </object>
<object class="GtkBox" id="_loadingSpinner">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="spacing">5</property>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Loading....</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface> </interface>

View File

@@ -0,0 +1,52 @@
using System;
using System.Threading.Tasks;
using AutoMapper;
using EstusShots.Server.Models;
using EstusShots.Server.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace EstusShots.Server.Controllers
{
[ApiController]
[Route("/api/[controller]")]
public class EpisodeController : ControllerBase
{
private readonly EstusShotsContext _context;
private readonly IMapper _mapper;
private readonly ILogger _logger;
public EpisodeController(EstusShotsContext context, IMapper mapper, ILogger<EpisodeController> logger)
{
_context = context;
_mapper = mapper;
_logger = logger;
}
[HttpGet("{id}")]
public async Task<ActionResult<Shared.Dto.Episode>> GetEpisode(Guid id)
{
var episode = await _context.Seasons.FindAsync(id);
if (episode == null) {return NotFound();}
var episodeDto = _mapper.Map<Shared.Dto.Episode>(episode);
return episodeDto;
}
[HttpPost]
public async Task<ActionResult<Shared.Dto.Episode>> CreateSeason(Shared.Dto.Episode episodeDto)
{
var episode = _mapper.Map<Episode>(episodeDto);
_context.Episodes.Add(episode);
try
{
await _context.SaveChangesAsync();
}
catch (Exception e)
{
_logger.LogError(e, "Error while saving object");
}
return CreatedAtAction(nameof(GetEpisode), new {id = episode.EpisodeId}, episode);
}
}
}

View File

@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AutoMapper;
using EstusShots.Server.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Dto = EstusShots.Shared.Dto;
namespace EstusShots.Server.Controllers
{
[ApiController]
[Route("/api/[controller]")]
public class EpisodesController : ControllerBase
{
private readonly EstusShotsContext _context;
private readonly IMapper _mapper;
private readonly ILogger _logger;
public EpisodesController(ILogger<EpisodesController> logger, IMapper mapper, EstusShotsContext context)
{
_logger = logger;
_mapper = mapper;
_context = context;
}
[HttpGet("seasonId")]
public async Task<ActionResult<List<Dto.Episode>>> GetEpisodes(Guid seasonId)
{
_logger.LogDebug($"All");
var episodes = await _context.Episodes.Where(x => x.SeasonId == seasonId).ToListAsync();
var dtos = _mapper.Map<List<Dto.Episode>>(episodes);
return dtos;
}
}
}

View File

@@ -4,7 +4,8 @@ using AutoMapper;
using EstusShots.Server.Models; using EstusShots.Server.Models;
using EstusShots.Server.Services; using EstusShots.Server.Services;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Dto = EstusShots.Shared.Models; using Microsoft.Extensions.Logging;
using Dto = EstusShots.Shared.Dto;
namespace EstusShots.Server.Controllers namespace EstusShots.Server.Controllers
{ {
@@ -14,18 +15,20 @@ namespace EstusShots.Server.Controllers
{ {
private readonly EstusShotsContext _context; private readonly EstusShotsContext _context;
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly ILogger _logger;
public SeasonController(EstusShotsContext context, IMapper mapper) public SeasonController(EstusShotsContext context, IMapper mapper, ILogger<SeasonController> logger)
{ {
_context = context; _context = context;
_mapper = mapper; _mapper = mapper;
_logger = logger;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<ActionResult<Dto.Season>> GetSeason(Guid id) public async Task<ActionResult<Dto.Season>> GetSeason(Guid id)
{ {
var season = await _context.Seasons.FindAsync(id); var season = await _context.Seasons.FindAsync(id);
if (season == null) return NotFound(); if (season == null) {return NotFound();}
var seasonDto = _mapper.Map<Dto.Season>(season); var seasonDto = _mapper.Map<Dto.Season>(season);
return seasonDto; return seasonDto;
@@ -36,7 +39,15 @@ namespace EstusShots.Server.Controllers
{ {
var dbSeason = _mapper.Map<Season>(season); var dbSeason = _mapper.Map<Season>(season);
_context.Seasons.Add(dbSeason); _context.Seasons.Add(dbSeason);
try
{
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
_logger.LogInformation("New season created");
}
catch (Exception e)
{
_logger.LogError(e, "Error while saving Season");
}
return CreatedAtAction(nameof(GetSeason), new {id = dbSeason.SeasonId}, dbSeason); return CreatedAtAction(nameof(GetSeason), new {id = dbSeason.SeasonId}, dbSeason);
} }
} }

View File

@@ -4,7 +4,7 @@ using AutoMapper;
using EstusShots.Server.Services; using EstusShots.Server.Services;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Dto = EstusShots.Shared.Models; using Dto = EstusShots.Shared.Dto;
namespace EstusShots.Server.Controllers namespace EstusShots.Server.Controllers
{ {

View File

@@ -7,8 +7,11 @@ namespace EstusShots.Server.Mapping
{ {
public Profiles() public Profiles()
{ {
CreateMap<Season, Shared.Models.Season>(); CreateMap<Season, Shared.Dto.Season>();
CreateMap<Shared.Models.Season, Season>(); CreateMap<Shared.Dto.Season, Season>();
CreateMap<Episode, Shared.Dto.Episode>();
CreateMap<Shared.Dto.Episode, Episode>();
} }
} }
} }

View File

@@ -0,0 +1,24 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace EstusShots.Server.Models
{
public class Episode
{
public Guid EpisodeId { get; set; }
public int Number { get; set; }
[MaxLength(50)] public string Title { get; set; } = default!;
public DateTime DateTime { get; set; }
public DateTime Start { get; set; }
public DateTime? End { get; set; }
public Guid SeasonId { get; set; }
public Season Season { get; set; } = default!;
}
}

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace EstusShots.Server.Models namespace EstusShots.Server.Models
@@ -16,5 +17,7 @@ namespace EstusShots.Server.Models
public DateTime Start { get; set; } public DateTime Start { get; set; }
public DateTime? End { get; set; } public DateTime? End { get; set; }
public List<Episode> Episodes { get; set; } = default!;
} }
} }

View File

@@ -18,6 +18,7 @@ namespace EstusShots.Server
public static IHostBuilder CreateHostBuilder(string[] args) => public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args) Host.CreateDefaultBuilder(args)
.ConfigureLogging(builder => { builder.AddConsole(); })
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
} }
} }

View File

@@ -7,8 +7,10 @@ namespace EstusShots.Server.Services
{ {
public EstusShotsContext(DbContextOptions options) : base(options) public EstusShotsContext(DbContextOptions options) : base(options)
{ {
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
} }
public DbSet<Season> Seasons { get; set; } = default!; public DbSet<Season> Seasons { get; set; } = default!;
public DbSet<Episode> Episodes { get; set; } = default!;
} }
} }

View File

@@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace EstusShots.Server namespace EstusShots.Server
{ {

View File

@@ -0,0 +1,25 @@
using System;
namespace EstusShots.Shared.Dto
{
public class Episode
{
public Guid EpisodeId { get; set; }
public int Number { get; set; }
public string Title { get; set; }
public DateTime DateTime { get; set; }
public DateTime Start { get; set; }
public DateTime? End { get; set; }
public Guid SeasonId { get; set; }
public Season Season { get; set; }
public string Displayname => $"S{Season.Number:D2}E{Number:D2}";
}
}

View File

@@ -1,6 +1,6 @@
using System; using System;
namespace EstusShots.Shared.Models namespace EstusShots.Shared.Dto
{ {
public class Season public class Season
{ {

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<Nullable>enable</Nullable> <Nullable>disable</Nullable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">

View File

@@ -0,0 +1,32 @@
using System;
namespace EstusShots.Shared.Models
{
public class OperationResult
{
public bool Success { get; }
public string ShortMessage { get; set; }
public string DetailedMessage { get; set; }
public string StackTrace { get; set; }
public OperationResult()
{
Success = true;
}
public OperationResult(bool success, string shortMessage, string detailedMessage = null)
{
Success = success;
ShortMessage = shortMessage;
DetailedMessage = detailedMessage;
}
public OperationResult(Exception e)
{
Success = false;
ShortMessage = e.Message;
DetailedMessage = e.InnerException?.Message;
StackTrace = e.StackTrace;
}
}
}