ASP.NET API #1

Open
Zagrebin wants to merge 7 commits from develop into main
27 changed files with 635 additions and 11 deletions
Showing only changes of commit 312a75e43b - Show all commits

View File

@ -1,6 +1,7 @@
using domain.UseCase;
using Microsoft.AspNetCore.Mvc;
using Presence.API.Response;
using domain.Request;
using System;
using System.Linq;
@ -18,10 +19,10 @@ namespace Presence.API.Controllers
}
[HttpGet("/presence/{groupId}")]
public ActionResult<PresenceResponse> GetPresence(
public ActionResult<IEnumerable<PresenceResponse>> GetPresence(
int groupId,
[FromQuery] int? subject,
[FromQuery] DateTime? date,
[FromQuery] DateOnly? date,
[FromQuery] int? student)
{
var presences = _presenceUseCase.GetPresence(groupId, subject, date, student)
@ -32,11 +33,69 @@ namespace Presence.API.Controllers
StudentName = $"{p.Student.LastName} {p.Student.FirstName} {p.Student.Patronymic}",
SubjectId = p.SubjectId,
SubjectName = p.Subject.Name,
TrafficId = p.TrafficId,
TrafficName = p.Traffic.Name,
LessonNumber = p.LessonNumber,
Date = p.Date
})
.ToList();
return Ok(presences);
}
[HttpPost("/presence")]
public IActionResult AddPresenceRecords([FromBody] AddPresenceRequest request)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
try
{
_presenceUseCase.AddPresenceRecords(request);
return Ok();
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
}
[HttpPut("/presence")]
public IActionResult UpdatePresenceRecords([FromBody] UpdatePresenceRequest request)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
try
{
_presenceUseCase.UpdatePresenceRecords(request);
return Ok();
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
}
[HttpDelete("/presence")]
public IActionResult DeleteAllPresence()
{
_presenceUseCase.DeleteAllPresence();
return NoContent();
}
[HttpDelete("/presence/group")]
public IActionResult DeleteGroupPresence([FromQuery] int? group)
{
if (!group.HasValue)
{
_presenceUseCase.DeleteAllPresence();
}
else
{
_presenceUseCase.DeleteGroupPresence(group.Value);
}
return NoContent();
}
}
}

View File

@ -9,6 +9,9 @@ namespace Presence.API.Response
public string StudentName { get; set; }
public int SubjectId { get; set; }
public string SubjectName { get; set; }
public int TrafficId { get; set; }
public string TrafficName { get; set; }
public int LessonNumber { get; set; }
public DateOnly Date { get; set; }
}
}

View File

@ -0,0 +1,15 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Presence.Desktop.App"
xmlns:local="using:Presence.Desktop"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

View File

@ -0,0 +1,36 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using Presence.Desktop.DI;
using Presence.Desktop.ViewModels;
using Presence.Desktop.Views;
namespace Presence.Desktop
{
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddComonoServices();
var services = serviceCollection.BuildServiceProvider();
MainWindowViewModel mainViewModel = services.GetRequiredService<MainWindowViewModel>();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = mainViewModel,
};
}
base.OnFrameworkInitializationCompleted();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

View File

@ -0,0 +1,21 @@
using data;
using data.Repository;
using domain.UseCase;
using domain.Service;
using Microsoft.Extensions.DependencyInjection;
using Presence.Desktop.ViewModels;
namespace Presence.Desktop.DI
{
public static class ServiceCollectionExtension
{
public static void AddComonoServices(this IServiceCollection collection)
{
collection
.AddDbContext<DatabaseContext>()
.AddSingleton<IGroupRepository, SQLGroupRepository>()
.AddTransient<IGroupUseCase, GroupService>()
.AddTransient<MainWindowViewModel>();
}
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Presence.Desktop.Models
{
public class GroupPresenter
{
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<StudentPresenter>? students = null;
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Presence.Desktop.Models
{
public class StudentPresenter
{
public int Id { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public string Patronymic { get; set; }
public GroupPresenter Group { get; set; }
}
}

View File

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
<AvaloniaXaml Remove="Новая папка\**" />
<Compile Remove="Новая папка\**" />
<EmbeddedResource Remove="Новая папка\**" />
<None Remove="Новая папка\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.1.0" />
<PackageReference Include="Avalonia.Desktop" Version="11.1.0" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.0" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.0" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.0" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\data\data.csproj" />
<ProjectReference Include="..\domain\domain.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,24 @@
using Avalonia;
using Avalonia.ReactiveUI;
using System;
namespace Presence.Desktop
{
internal sealed class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace()
.UseReactiveUI();
}
}

View File

@ -0,0 +1,34 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Presence.Desktop.ViewModels;
using System;
namespace Presence.Desktop
{
public class ViewLocator : IDataTemplate
{
public Control? Build(object? data)
{
if (data is null)
return null;
var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
var type = Type.GetType(name);
if (type != null)
{
var control = (Control)Activator.CreateInstance(type)!;
control.DataContext = data;
return control;
}
return new TextBlock { Text = "Not Found: " + name };
}
public bool Match(object? data)
{
return data is ViewModelBase;
}
}
}

View File

@ -0,0 +1,89 @@
using domain.UseCase;
using Presence.Desktop.Models;
using System;
using System.Linq;
using ReactiveUI;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Presence.Desktop.ViewModels
{
public class MainWindowViewModel : ViewModelBase
{
private readonly IGroupUseCase _groupService;
private List<GroupPresenter> groupPresenters = new List<GroupPresenter>();
private ObservableCollection<GroupPresenter> _groups;
public ObservableCollection<GroupPresenter> Groups
{
get => _groups;
set => this.RaiseAndSetIfChanged(ref _groups, value);
}
private GroupPresenter? _selectedGroupItem;
public GroupPresenter? SelectedGroupItem
{
get => _selectedGroupItem;
set => this.RaiseAndSetIfChanged(ref _selectedGroupItem, value);
}
private ObservableCollection<StudentPresenter> _students;
public ObservableCollection<StudentPresenter> Students
{
get => _students;
set => this.RaiseAndSetIfChanged(ref _students, value);
}
public MainWindowViewModel()
{
_groupService = null;
_groups = new();
_students = new();
}
public MainWindowViewModel(IGroupUseCase gService, IGroupUseCase groupService, ObservableCollection<GroupPresenter> groups, ObservableCollection<StudentPresenter> students)
{
_groupService = gService;
foreach (var item in _groupService.GetGroupsWithStudents())
{
GroupPresenter groupPresenter = new GroupPresenter
{
Id = item.Id,
Name = item.Name,
students = item.Users?.Select(u => new StudentPresenter
{
Id = u.Id,
FirstName = u.FirstName,
LastName = u.LastName,
Patronymic = u.Patronymic,
Group = new GroupPresenter
{
Id = item.Id,
Name = item.Name
}
})
};
groupPresenters.Add(groupPresenter);
}
_groups = new(groupPresenters);
_students = new();
this.WhenAnyValue(vm => vm.SelectedGroupItem)
.Subscribe(_ => SetStudents());
}
private void SetStudents()
{
if (SelectedGroupItem == null) return;
if (SelectedGroupItem.students == null) return;
Students.Clear();
foreach (var student in SelectedGroupItem.students)
Students.Add(student);
}
}
}

View File

@ -0,0 +1,8 @@
using ReactiveUI;
namespace Presence.Desktop.ViewModels
{
public class ViewModelBase : ReactiveObject
{
}
}

View File

@ -0,0 +1,54 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Presence.Desktop.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Presence.Desktop.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Icon="/Assets/avalonia-logo.ico"
Title="Presence.Desktop">
<Design.DataContext>
<vm:MainWindowViewModel/>
</Design.DataContext>
<DockPanel Background="Azure">
<StackPanel DockPanel.Dock="Bottom">
<TextBlock Text="List ↑" HorizontalAlignment="Center"/>
</StackPanel>
<StackPanel
Spacing="10"
HorizontalAlignment="Center"
DockPanel.Dock="Top"
Orientation="Horizontal">
<ComboBox ItemsSource="{Binding Groups}" SelectedValue="{Binding SelectedGroupItem}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ComboBox/>
<ComboBox/>
</StackPanel>
<Border>
<ListBox Background="Bisque" ItemsSource="{Binding Students}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock>
<MultiBinding StringFormat="{}{0} {1} {2}">
<Binding Path="LastName"/>
<Binding Path="FirstName"/>
<Binding Path="Patronymic"/>
</MultiBinding>
</TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</DockPanel>
</Window>

View File

@ -0,0 +1,12 @@
using Avalonia.Controls;
namespace Presence.Desktop.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="Presence.Desktop.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

View File

@ -12,12 +12,14 @@ namespace data.DAO
[Key]
public int Id { get; set; }
public string DiaryText { get; set; }
public string? DiaryText { get; set; }
public int TrafficId { get; set; }
public int StudentGroupSubjectId { get; set; }
public int StudentId { get; set; }
public virtual Traffic Traffic { get; set; }
public virtual StudentGroupSubject StudentGroupSubject { get; set; }

View File

@ -9,7 +9,15 @@ namespace data.Repository
IEnumerable<Diary> GetPresence(
int groupId,
int? subjectId = null,
DateTime? date = null,
DateOnly? date = null,
int? studentId = null);
public void DeleteAllPresence();
public void DeleteGroupPresence(int groupId);
public void AddPresenceRecords(List<Diary> items);
public void UpdatePresenceRecords(List<Diary> items);
}
}

View File

@ -1,7 +1,7 @@
using data.DAO;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
namespace data.Repository
@ -18,25 +18,66 @@ namespace data.Repository
public IEnumerable<Diary> GetPresence(
int groupId,
int? subjectId = null,
DateTime? date = null,
DateOnly? date = null,
int? studentId = null)
{
var query = _context.Diaries
.Include(p => p.Student)
.Include(p => p.StudentGroupSubject)
.ThenInclude(p => p.Subject)
.Include(p => p.Traffic)
.Where(p => p.Student.GroupId == groupId);
if (subjectId.HasValue)
query = query.Where(p => p.StudentGroupSubject.Subject.Id == subjectId.Value);
if (date.HasValue)
query = query.Where(p => p.Date == DateOnly.FromDateTime(date.Value));
query = query.Where(p => p.Date == date.Value);
if (studentId.HasValue)
query = query.Where(p => p.Student.Id == studentId.Value);
return query.ToList();
}
public void DeleteAllPresence()
{
_context.Diaries.RemoveRange(_context.Diaries);
_context.SaveChanges();
}
public void DeleteGroupPresence(int groupId)
{
var presencesToDelete = _context.Diaries
.Include(p => p.Student)
.Where(p => p.Student.GroupId == groupId);
_context.Diaries.RemoveRange(presencesToDelete);
_context.SaveChanges();
}
public void AddPresenceRecords(List<Diary> items)
{
_context.Diaries.AddRange(items);
_context.SaveChanges();
}
public void UpdatePresenceRecords(List<Diary> items)
{
foreach (var item in items)
{
var existingRecord = _context.Diaries
.FirstOrDefault(p =>
p.Date == item.Date &&
p.NumberSubject == item.NumberSubject &&
p.StudentId == item.StudentId);
if (existingRecord != null)
{
existingRecord.TrafficId = item.TrafficId;
}
}
_context.SaveChanges();
}
}
}

View File

@ -9,6 +9,9 @@ namespace domain.Entity
public UserEntity Student { get; set; }
public int SubjectId { get; set; }
public SubjectEntity Subject { get; set; }
public int TrafficId { get; set; }
public TrafficEntity Traffic { get; set; }
public int LessonNumber { get; set; }
public DateOnly Date { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace domain.Entity
{
public class TrafficEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
namespace domain.Request
{
public class AddPresenceRequest
{
public List<PresenceItem> Items { get; set; } = new List<PresenceItem>();
}
public class PresenceItem
{
public DateOnly Date { get; set; }
public int StudentGroupSubjectId { get; set; }
public int LessonNumber { get; set; }
public int StudentId { get; set; }
public int TrafficId { get; set; }
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
namespace domain.Request
{
public class UpdatePresenceRequest
{
public List<UpdatePresenceItem> Items { get; set; } = new List<UpdatePresenceItem>();
}
public class UpdatePresenceItem
{
public DateOnly Date { get; set; }
public int LessonNumber { get; set; }
public int StudentId { get; set; }
public int TrafficId { get; set; }
}
}

View File

@ -1,5 +1,7 @@
using data.DAO;
using data.Repository;
using domain.Entity;
using domain.Request;
using domain.UseCase;
using System;
using System.Collections.Generic;
@ -18,7 +20,7 @@ namespace domain.Service
public IEnumerable<PresenceEntity> GetPresence(
int groupId,
int? subjectId = null,
DateTime? date = null,
DateOnly? date = null,
int? studentId = null)
{
return _presenceRepository.GetPresence(groupId, subjectId, date, studentId).Select(p => new PresenceEntity
@ -39,8 +41,69 @@ namespace domain.Service
Id = p.StudentGroupSubject.Subject.Id,
Name = p.StudentGroupSubject.Subject.Name
},
TrafficId = p.TrafficId,
Traffic = new TrafficEntity
{
Id = p.TrafficId,
Name = p.Traffic.Name
},
LessonNumber = p.NumberSubject,
Date = p.Date
});
}
public void DeleteAllPresence()
{
_presenceRepository.DeleteAllPresence();
}
public void DeleteGroupPresence(int groupId)
{
_presenceRepository.DeleteGroupPresence(groupId);
}
public void AddPresenceRecords(AddPresenceRequest request)
{
if (request?.Items == null || request.Items.Count == 0)
throw new ArgumentException("No records");
List<Diary> diaries = new List<Diary>();
foreach (var item in request.Items)
{
diaries.Add(new Diary
{
Date = item.Date,
DiaryText = "",
NumberSubject = item.LessonNumber,
StudentId = item.StudentId,
TrafficId = item.TrafficId,
StudentGroupSubjectId = item.StudentGroupSubjectId
});
}
_presenceRepository.AddPresenceRecords(diaries);
}
public void UpdatePresenceRecords(UpdatePresenceRequest request)
{
if (request?.Items == null || request.Items.Count == 0)
throw new ArgumentException("No records");
List<Diary> diaries = new List<Diary>();
foreach (var item in request.Items)
{
diaries.Add(new Diary
{
Date = item.Date,
NumberSubject = item.LessonNumber,
StudentId = item.StudentId,
TrafficId = item.TrafficId,
});
}
_presenceRepository.UpdatePresenceRecords(diaries);
}
}
}

View File

@ -1,4 +1,5 @@
using domain.Entity;
using domain.Request;
using System;
using System.Collections.Generic;
@ -9,7 +10,12 @@ namespace domain.UseCase
IEnumerable<PresenceEntity> GetPresence(
int groupId,
int? subjectId = null,
DateTime? date = null,
DateOnly? date = null,
int? studentId = null);
void DeleteAllPresence();
void DeleteGroupPresence(int groupId);
void AddPresenceRecords(AddPresenceRequest request);
void UpdatePresenceRecords(UpdatePresenceRequest request);
}
}

View File

@ -7,7 +7,9 @@
</PropertyGroup>
<ItemGroup>
<Folder Include="Response\" />
<Compile Remove="Response\**" />
<EmbeddedResource Remove="Response\**" />
<None Remove="Response\**" />
</ItemGroup>
<ItemGroup>

View File

@ -9,7 +9,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "console_ui", "console_ui\co
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "domain", "domain\domain.csproj", "{4D39120E-C021-4D38-BB4D-2F58BDE5FE17}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Presence.API", "Presence.API\Presence.API.csproj", "{88AE15FA-7E2D-4D7C-A75B-7DAC9742B32A}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Presence.API", "Presence.API\Presence.API.csproj", "{88AE15FA-7E2D-4D7C-A75B-7DAC9742B32A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Presence.Desktop", "Presence.Desktop\Presence.Desktop.csproj", "{03BA523C-7C5B-4FE1-BAC1-461E4B4F7FCD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -33,6 +35,10 @@ Global
{88AE15FA-7E2D-4D7C-A75B-7DAC9742B32A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88AE15FA-7E2D-4D7C-A75B-7DAC9742B32A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{88AE15FA-7E2D-4D7C-A75B-7DAC9742B32A}.Release|Any CPU.Build.0 = Release|Any CPU
{03BA523C-7C5B-4FE1-BAC1-461E4B4F7FCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{03BA523C-7C5B-4FE1-BAC1-461E4B4F7FCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{03BA523C-7C5B-4FE1-BAC1-461E4B4F7FCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{03BA523C-7C5B-4FE1-BAC1-461E4B4F7FCD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE