shnaider_code 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/CODE_OF_CONDUCT.md +84 -0
  4. data/Documentation.md +33 -0
  5. data/Gemfile +13 -0
  6. data/Gemfile.lock +92 -0
  7. data/lib/shnaider_code/version.rb +5 -0
  8. data/lib/shnaider_code.rb +10 -0
  9. data/lib/source/controllers/student_input_form/student_input_form_controller_create.rb +68 -0
  10. data/lib/source/controllers/student_input_form/student_input_form_controller_edit.rb +78 -0
  11. data/lib/source/controllers/tab_students_controller.rb +104 -0
  12. data/lib/source/db_config/config.example.yaml +5 -0
  13. data/lib/source/db_config/migrations/001_create_table_student.sql +12 -0
  14. data/lib/source/db_config/mock_data/fill_student.sql +6 -0
  15. data/lib/source/models/student.rb +125 -0
  16. data/lib/source/models/student_base.rb +128 -0
  17. data/lib/source/models/student_short.rb +58 -0
  18. data/lib/source/repositories/adapters/db_source_adapter.rb +54 -0
  19. data/lib/source/repositories/adapters/file_source_adapter.rb +37 -0
  20. data/lib/source/repositories/containers/data_list.rb +74 -0
  21. data/lib/source/repositories/containers/data_list_student_short.rb +18 -0
  22. data/lib/source/repositories/containers/data_table.rb +35 -0
  23. data/lib/source/repositories/data_sources/db_data_source.rb +35 -0
  24. data/lib/source/repositories/data_sources/file_data_source.rb +77 -0
  25. data/lib/source/repositories/data_sources/transformers/data_transformer_base.rb +15 -0
  26. data/lib/source/repositories/data_sources/transformers/data_transformer_json.rb +16 -0
  27. data/lib/source/repositories/data_sources/transformers/data_transformer_yaml.rb +16 -0
  28. data/lib/source/repositories/student_repository.rb +37 -0
  29. data/lib/source/util/logger_holder.rb +29 -0
  30. data/shnaider_code-1.1.4.gem +0 -0
  31. data/shnaider_code.gemspec +17 -0
  32. data/sig/shnaider_code.rbs +4 -0
  33. metadata +88 -0
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Абстрактный класс с базовым описанием студента
5
+
6
+ class StudentBase
7
+ private_class_method :new
8
+
9
+ ##
10
+ # Валидация имени (также применимо к фамилии и отчеству)
11
+
12
+ def self.valid_name?(name)
13
+ name.match(/(^[А-Я][а-я]+$)|(^[A-Z][a-z]+$)/)
14
+ end
15
+
16
+ ##
17
+ # Валидация номера телефона
18
+
19
+ def self.valid_phone?(phone)
20
+ phone.match(/^\+?[78] ?[(-]?\d{3} ?[)-]?[ -]?\d{3}[ -]?\d{2}[ -]?\d{2}$/)
21
+ end
22
+
23
+ ##
24
+ # Валидация имени профиля пользователя
25
+
26
+ def self.valid_profile_name?(profile_name)
27
+ profile_name.match(/^[a-zA-Z0-9_.]+$/)
28
+ end
29
+
30
+ ##
31
+ # Валидация email
32
+
33
+ def self.valid_email?(email)
34
+ email.match(/^(?:[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/)
35
+ end
36
+
37
+ protected
38
+
39
+ attr_writer :id
40
+ attr_reader :phone, :telegram, :email
41
+
42
+ public
43
+
44
+ attr_reader :id, :git
45
+
46
+ ##
47
+ # Стандартный конструктор. Принимает именованные параметры:
48
+ # id - Id студента
49
+ # phone - Телефон
50
+ # telegram - Ник в телеграме
51
+ # email - Электронная почта
52
+ # git - Ник на гите
53
+
54
+ def initialize(id: nil, phone: nil, telegram: nil, email: nil, git: nil)
55
+ self.id = id
56
+ self.phone = phone
57
+ self.telegram = telegram
58
+ self.email = email
59
+ self.git = git
60
+ end
61
+
62
+ ##
63
+ # Возвращает первый доступный контакт пользователя в виде хеша.
64
+ # Пример: {type: :telegram, value: 'xoxolovelylove'}
65
+
66
+ def short_contact
67
+ contact = {}
68
+ %i[telegram email phone].each do |attr|
69
+ attr_val = send(attr)
70
+ next if attr_val.nil?
71
+
72
+ contact[:type] = attr
73
+ contact[:value] = attr_val
74
+ return contact
75
+ end
76
+
77
+ nil
78
+ end
79
+
80
+ protected
81
+
82
+ def phone=(new_phone)
83
+ raise ArgumentError, "Invalid argument: phone=#{new_phone}" unless new_phone.nil? || StudentBase.valid_phone?(new_phone)
84
+
85
+ @phone = new_phone
86
+ end
87
+
88
+ def telegram=(new_telegram)
89
+ raise ArgumentError, "Invalid argument: telegram=#{new_telegram}" unless new_telegram.nil? || StudentBase.valid_profile_name?(new_telegram)
90
+
91
+ @telegram = new_telegram
92
+ end
93
+
94
+ def git=(new_git)
95
+ raise ArgumentError, "Invalid argument: git=#{new_git}" unless new_git.nil? || StudentBase.valid_profile_name?(new_git)
96
+
97
+ @git = new_git
98
+ end
99
+
100
+ def email=(new_email)
101
+ raise ArgumentError, "Invalid argument: email=#{new_email}" unless new_email.nil? || StudentBase.valid_email?(new_email)
102
+
103
+ @email = new_email
104
+ end
105
+
106
+ public
107
+
108
+ ##
109
+ # Возвращает true, если у студента есть хотя бы один из контактов
110
+
111
+ def has_contacts?
112
+ !phone.nil? || !telegram.nil? || !email.nil?
113
+ end
114
+
115
+ ##
116
+ # Возвращает true, если у студента есть гит
117
+
118
+ def has_git?
119
+ !git.nil?
120
+ end
121
+
122
+ ##
123
+ # Возвращает true, если у студента есть хотя бы один из контактов и гит
124
+
125
+ def valid?
126
+ has_contacts? && has_git?
127
+ end
128
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Модель с краткой информацией о студенте
5
+
6
+ class StudentShort < StudentBase
7
+ public_class_method :new
8
+
9
+ private
10
+
11
+ attr_writer :last_name_and_initials, :contact
12
+
13
+ public
14
+
15
+ attr_reader :last_name_and_initials, :contact
16
+
17
+ ##
18
+ # Конструктор из объекта класса Student
19
+
20
+ def self.from_student(student)
21
+ raise ArgumentError, 'Student ID is required' if student.id.nil?
22
+
23
+ StudentShort.new(student.id, student.short_info)
24
+ end
25
+
26
+ ##
27
+ # Стандартный конструктор. Принимает:
28
+ # id - Числовой id студента
29
+ # info_str - JSON строка с полями last_name_and_initials (обязательно), contact, git, а также полями базового класса
30
+
31
+ def initialize(id, info_str)
32
+ params = JSON.parse(info_str, { symbolize_names: true })
33
+ raise ArgumentError, 'Fields required: last_name_and_initials' if !params.key?(:last_name_and_initials) || params[:last_name_and_initials].nil?
34
+
35
+ self.id = id
36
+ self.last_name_and_initials = params[:last_name_and_initials]
37
+ self.contact = params[:contact]
38
+ self.git = params[:git]
39
+
40
+ options = {}
41
+ options[:id] = id
42
+ options[:git] = git
43
+ options[contact[:type].to_sym] = contact[:value] if contact
44
+ super(**options)
45
+ end
46
+
47
+ ##
48
+ # Преобразование объекта в строку
49
+
50
+ def to_s
51
+ result = last_name_and_initials
52
+ %i[id contact git].each do |attr|
53
+ attr_val = send(attr)
54
+ result += ", #{attr}=#{attr_val}" unless attr_val.nil?
55
+ end
56
+ result
57
+ end
58
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './LabStudents/repositories/data_sources/db_data_source'
4
+ require './LabStudents/models/student'
5
+ require './LabStudents/models/student_short'
6
+ require './LabStudents/repositories/containers/data_list_student_short'
7
+
8
+ class DBSourceAdapter
9
+ def initialize
10
+ @db = DBDataSource.instance
11
+ end
12
+
13
+ def student_by_id(student_id)
14
+ hash = @db.prepare_exec('SELECT * FROM student WHERE id = ?', student_id).first
15
+ return nil if hash.nil?
16
+
17
+ Student.from_hash(hash)
18
+ end
19
+
20
+ def paginated_short_students(page, count, existing_data_list = nil)
21
+ offset = (page - 1) * count
22
+ students = @db.prepare_exec('SELECT * FROM student LIMIT ?, ?', offset, count)
23
+ slice = students.map { |h| StudentShort.from_student(Student.from_hash(h)) }
24
+ return DataListStudentShort.new(slice) if existing_data_list.nil?
25
+
26
+ existing_data_list.replace_objects(slice)
27
+ existing_data_list
28
+ end
29
+
30
+ def add_student(student)
31
+ template = 'INSERT INTO student(last_name, first_name, father_name, phone, telegram, email, git) VALUES (?, ?, ?, ?, ?, ?, ?)'
32
+ @db.prepare_exec(template, *student_fields(student))
33
+ @db.query('SELECT LAST_INSERT_ID()').first.first[1]
34
+ end
35
+
36
+ def replace_student(student_id, student)
37
+ template = 'UPDATE student SET last_name=?, first_name=?, father_name=?, phone=?, telegram=?, email=?, git=? WHERE id=?'
38
+ @db.prepare_exec(template, *student_fields(student), student_id)
39
+ end
40
+
41
+ def remove_student(student_id)
42
+ @db.prepare_exec('DELETE FROM student WHERE id = ?', student_id)
43
+ end
44
+
45
+ def student_count
46
+ @db.query('SELECT COUNT(id) FROM student').first.first[1]
47
+ end
48
+
49
+ private
50
+
51
+ def student_fields(student)
52
+ [student.last_name, student.first_name, student.father_name, student.phone, student.telegram, student.email, student.git]
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FileSourceAdapter
4
+ def initialize(data_transformer, file_path)
5
+ @file_path = file_path
6
+ @file_source = FileDataSource.new(data_transformer)
7
+ @file_source.load_from_file(file_path)
8
+ end
9
+
10
+ def student_by_id(student_id)
11
+ @file_source.student_by_id(student_id)
12
+ end
13
+
14
+ def paginated_short_students(page, count, existing_data_list = nil)
15
+ @file_source.paginated_short_students(page, count, existing_data_list)
16
+ end
17
+
18
+ def add_student(student)
19
+ added_id = @file_source.add_student(student)
20
+ @file_source.save_to_file(@file_path)
21
+ added_id
22
+ end
23
+
24
+ def replace_student(student_id, student)
25
+ @file_source.replace_student(student_id, student)
26
+ @file_source.save_to_file(@file_path)
27
+ end
28
+
29
+ def remove_student(student_id)
30
+ @file_source.remove_student(student_id)
31
+ @file_source.save_to_file(@file_path)
32
+ end
33
+
34
+ def student_count
35
+ @file_source.student_count
36
+ end
37
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './LabStudents/repositories/containers/data_table'
4
+
5
+ class DataList
6
+ # Это "абстрактный" класс
7
+ private_class_method :new
8
+
9
+ attr_writer :objects
10
+
11
+ # Конструктор, принимает массив любых объектов
12
+ def initialize(objects)
13
+ self.objects = objects
14
+ @listeners = []
15
+ end
16
+
17
+ def add_listener(listener)
18
+ @listeners << listener
19
+ end
20
+
21
+ def remove_listener(listener)
22
+ @listeners.delete(listener)
23
+ end
24
+
25
+ def notify
26
+ @listeners.each { |lst| lst.on_datalist_changed(data_table) }
27
+ end
28
+
29
+ # Выбрать элемент по номеру
30
+ def select_element(number)
31
+ self.selected_num = number < objects.size ? number : nil
32
+ end
33
+
34
+ def selected_id
35
+ objects[selected_num].id
36
+ end
37
+
38
+ # Получить DataTable со всеми элементами.
39
+ def data_table
40
+ result = []
41
+ counter = 0
42
+ objects.each do |obj|
43
+ row = []
44
+ row << counter
45
+ row.push(*table_fields(obj))
46
+ result << row
47
+ counter += 1
48
+ end
49
+ DataTable.new(result)
50
+ end
51
+
52
+ # Добавить элементы в конец списка
53
+ def replace_objects(objects)
54
+ self.objects = objects.dup
55
+ notify
56
+ end
57
+
58
+ protected
59
+
60
+ # Список значений полей для DataTable. Переопределить в наследниках
61
+ def table_fields(_obj)
62
+ []
63
+ end
64
+
65
+ # Имена атрибутов объектов по порядку. Переопределить в наследниках
66
+ def column_names
67
+ []
68
+ end
69
+
70
+ private
71
+
72
+ attr_reader :objects
73
+ attr_accessor :selected_num
74
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'data_list'
4
+
5
+ class DataListStudentShort < DataList
6
+ # Делаем приватный new предка публичным
7
+ public_class_method :new
8
+
9
+ def column_names
10
+ ['Фамилия И. О.', 'Гит', 'Контакт']
11
+ end
12
+
13
+ protected
14
+
15
+ def table_fields(obj)
16
+ [obj.last_name_and_initials, obj.git, obj.contact]
17
+ end
18
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DataTable
4
+ attr_reader :rows_count, :cols_count
5
+
6
+ # Конструктор, принимает 2D Array
7
+ def initialize(table)
8
+ self.rows_count = table.size
9
+ max_cols = 0
10
+ table.each { |row| max_cols = row.size if row.size > max_cols }
11
+ self.cols_count = max_cols
12
+ self.table = table
13
+ end
14
+
15
+ # Получить значение в ячейке [row, col]
16
+ def get_item(row, col)
17
+ return nil if row >= rows_count
18
+ return nil if col >= cols_count
19
+
20
+ table[row][col].dup
21
+ end
22
+
23
+ def to_2d_array
24
+ table.dup
25
+ end
26
+
27
+ def to_s
28
+ "DataTable (#{rows_count}x#{cols_count})"
29
+ end
30
+
31
+ private
32
+
33
+ attr_accessor :table
34
+ attr_writer :rows_count, :cols_count
35
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mysql2'
4
+
5
+ ##
6
+ # Источник данных из БД
7
+
8
+ class DBDataSource
9
+ private_class_method :new
10
+ @instance_mutex = Mutex.new
11
+
12
+ def initialize
13
+ db_config = YAML.load_file('./LabStudents/db_config/config.example.yaml').transform_keys(&:to_sym)
14
+ @client = Mysql2::Client.new(db_config)
15
+ @client.query_options.merge!(symbolize_keys: true)
16
+ end
17
+
18
+ def self.instance
19
+ return @instance if @instance
20
+
21
+ @instance_mutex.synchronize do
22
+ @instance ||= new
23
+ end
24
+
25
+ @instance
26
+ end
27
+
28
+ def prepare_exec(statement, *params)
29
+ @client.prepare(statement).execute(*params)
30
+ end
31
+
32
+ def query(statement)
33
+ @client.query(statement)
34
+ end
35
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './LabStudents/models/student'
4
+ require './LabStudents/models/student_short'
5
+ require './LabStudents/repositories/containers/data_list_student_short'
6
+
7
+ ##
8
+ # Источник данных из файла
9
+
10
+ class FileDataSource
11
+ attr_writer :data_transformer
12
+
13
+ def initialize(data_transformer)
14
+ self.students = []
15
+ self.seq_id = 1
16
+ self.data_transformer = data_transformer
17
+ end
18
+
19
+ def load_from_file(file_path)
20
+ hash_list = data_transformer.str_to_hash_list(File.read(file_path))
21
+ self.students = hash_list.map { |h| Student.from_hash(h) }
22
+ update_seq_id
23
+ end
24
+
25
+ def save_to_file(file_path)
26
+ hash_list = students.map(&:to_hash)
27
+ File.write(file_path, data_transformer.hash_list_to_str(hash_list))
28
+ end
29
+
30
+ def student_by_id(student_id)
31
+ students.detect { |s| s.id == student_id }
32
+ end
33
+
34
+ def paginated_short_students(page, count, existing_data_list = nil)
35
+ offset = (page - 1) * count
36
+ slice = students[offset, count].map { |s| StudentShort.from_student(s) }
37
+
38
+ return DataListStudentShort.new(slice) if existing_data_list.nil?
39
+
40
+ existing_data_list.replace_objects(slice)
41
+ existing_data_list
42
+ end
43
+
44
+ def sorted
45
+ students.sort_by(&:last_name_and_initials)
46
+ end
47
+
48
+ def add_student(student)
49
+ student.id = seq_id
50
+ students << student
51
+ self.seq_id += 1
52
+ student.id
53
+ end
54
+
55
+ def replace_student(student_id, student)
56
+ idx = students.find_index { |s| s.id == student_id }
57
+ students[idx] = student
58
+ end
59
+
60
+ def remove_student(student_id)
61
+ students.reject! { |s| s.id == student_id }
62
+ end
63
+
64
+ def student_count
65
+ students.count
66
+ end
67
+
68
+ private
69
+
70
+ # Метод для актуализации seq_id
71
+ def update_seq_id
72
+ self.seq_id = students.max_by(&:id).id + 1
73
+ end
74
+
75
+ attr_reader :data_transformer
76
+ attr_accessor :students, :seq_id
77
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DataTransformerBase
4
+ private_class_method :new
5
+
6
+ protected
7
+
8
+ def str_to_hash_list(str)
9
+ raise NotImplementedError('Should be implemented in child')
10
+ end
11
+
12
+ def hash_list_to_str(hash_list)
13
+ raise NotImplementedError('Should be implemented in child')
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'data_transformer_base'
4
+ require 'json'
5
+
6
+ class DataTransformerJSON < DataTransformerBase
7
+ public_class_method :new
8
+
9
+ def str_to_hash_list(str)
10
+ JSON.parse(str, { symbolize_names: true })
11
+ end
12
+
13
+ def hash_list_to_str(hash_list)
14
+ JSON.pretty_generate(hash_list)
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'data_transformer_base'
4
+ require 'yaml'
5
+
6
+ class DataTransformerYAML < DataTransformerBase
7
+ public_class_method :new
8
+
9
+ def str_to_hash_list(str)
10
+ YAML.safe_load(str).map { |h| h.transform_keys(&:to_sym) }
11
+ end
12
+
13
+ def hash_list_to_str(hash_list)
14
+ hash_list.map { |h| h.transform_keys(&:to_s) }.to_yaml
15
+ end
16
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Репозиторий студентов с CRUD операциями.
5
+
6
+ class StudentRepository
7
+ def initialize(data_source_adapter)
8
+ @data_source_adapter = data_source_adapter
9
+ end
10
+
11
+ def student_by_id(student_id)
12
+ @data_source_adapter.student_by_id(student_id)
13
+ end
14
+
15
+ ##
16
+ # Получить page по счету count элементов (страница начинается с 1)
17
+
18
+ def paginated_short_students(page, count, existing_data_list = nil)
19
+ @data_source_adapter.paginated_short_students(page, count, existing_data_list)
20
+ end
21
+
22
+ def add_student(student)
23
+ @data_source_adapter.add_student(student)
24
+ end
25
+
26
+ def replace_student(student_id, student)
27
+ @data_source_adapter.replace_student(student_id, student)
28
+ end
29
+
30
+ def remove_student(student_id)
31
+ @data_source_adapter.remove_student(student_id)
32
+ end
33
+
34
+ def student_count
35
+ @data_source_adapter.student_count
36
+ end
37
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ ##
6
+ # Обертка для хранения объекта Logger
7
+
8
+ class LoggerHolder
9
+ private_class_method :new
10
+ @instance_mutex = Mutex.new
11
+
12
+ attr_reader :logger
13
+
14
+ def initialize
15
+ @logger = Logger.new('log.txt')
16
+
17
+ # @logger = Logger.new(STDOUT)
18
+ end
19
+
20
+ def self.instance
21
+ return @instance.logger if @instance
22
+
23
+ @instance_mutex.synchronize do
24
+ @instance ||= new
25
+ end
26
+
27
+ @instance.logger
28
+ end
29
+ end
Binary file
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/shnaider_code/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "shnaider_code"
7
+ spec.version = ShnaiderCode::VERSION
8
+ spec.authors = ["Shnaider"]
9
+ spec.email = ["nullexp.team@gmail.com"]
10
+ spec.summary = "Student App"
11
+ spec.description = "А gem that allows you to get pass for patterns"
12
+ spec.homepage = "https://github.com/bushmrz/learning_patterns_with_ruby"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 3.2.0"
15
+ spec.add_dependency 'win32api'
16
+ spec.files = Dir.glob("**/*")
17
+ end
@@ -0,0 +1,4 @@
1
+ module ShnaiderCode
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end