shnaider_code 1.1.5

Sign up to get free protection for your applications and to get access to all the features.
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