Skip to content

Commit

Permalink
Added a hint for openAI entry
Browse files Browse the repository at this point in the history
Added tests in user.rb

remove redundant code in spec files
  • Loading branch information
kkoehn authored and Melhaya committed Jul 11, 2024
1 parent b79cdab commit e1e1d45
Show file tree
Hide file tree
Showing 16 changed files with 105 additions and 51 deletions.
15 changes: 14 additions & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class User < ApplicationRecord
validates :email, presence: true, uniqueness: {case_sensitive: false}
validates :first_name, :last_name, :status_group, presence: true
validates :password_set, inclusion: [true, false]
validates :openai_api_key, allow_blank: true, length: {maximum: 255}
validate :validate_openai_api_key, if: -> { openai_api_key.present? }

has_many :tasks, dependent: :nullify

Expand Down Expand Up @@ -147,6 +147,19 @@ def to_s
name
end

def validate_openai_api_key
return unless openai_api_key_changed?

client = OpenAI::Client.new(access_token: openai_api_key)

begin
response = client.models.list
errors.add(:base, :invalid_api_key) unless response['data']
rescue Faraday::UnauthorizedError, OpenAI::Error
errors.add(:base, :invalid_api_key)
end
end

private

def avatar_format
Expand Down
2 changes: 1 addition & 1 deletion app/policies/task_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def manage?
end

def generate_test?
user.present? and user.openai_api_key.present? and update?
user&.openai_api_key.present?
end

private
Expand Down
2 changes: 1 addition & 1 deletion app/services/task_service/gpt_generate_tests.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def initialize(task:, openai_api_key:)
raise Gpt::MissingLanguageError if task.programming_language&.language.blank?

@task = task
@openai_api_key = openai_api_key.presence
@openai_api_key = openai_api_key
validate_api_key!
end

Expand Down
20 changes: 13 additions & 7 deletions app/views/tasks/show.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -573,15 +573,21 @@
i.fa-regular.fa-clone
=< t('common.button.duplicate')

- if policy(@task).generate_test?
= link_to generate_test_task_path(@task), method: :post, class: 'btn btn-important' do
i.fa-solid.fa-wand-magic-sparkles
= t('.button.generate_test')
- else
div data-bs-toggle='tooltip' title=t('.button.api_key_required') data-bs-delay='150'
= link_to '#', method: :post, class: 'btn btn-important disabled' do
- if policy(@task).update?
- if policy(@task).generate_test?
= link_to generate_test_task_path(@task),
method: :post,
class: 'btn btn-important' do
i.fa-solid.fa-wand-magic-sparkles
= t('.button.generate_test')
- else
div data-bs-toggle='tooltip' title=t('.button.api_key_required') data-bs-delay=150
= link_to generate_test_task_path(@task),
method: :post,
class: 'btn btn-important disabled' do
i.fa-solid.fa-wand-magic-sparkles
= t('.button.generate_test')


- if current_user.present?
= link_to t('common.button.back'), tasks_path, class: 'btn btn-important'
Expand Down
2 changes: 2 additions & 0 deletions app/views/users/registrations/edit.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
placeholder: t('users.show.openai_api_key'),
autocomplete: 'off',
class: 'form-control'
small.form-text.text-body-secondary
= t('.openai_api_key_usage_html', openai_api_link: 'https://platform.openai.com/api-keys')

= render 'avatar_form', f:

Expand Down
4 changes: 2 additions & 2 deletions app/views/users/show.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@
| :
.col.row-value
- if @user.openai_api_key.present?
= t('.provided_openai_key')
= t('.openai_api_key_provided')
- else
= t('.not_provided_openai_key')
= t('.openai_api_key_not_provided')
.row.vertical
.col.row-label
= t('.account_links.created')
Expand Down
2 changes: 1 addition & 1 deletion config/locales/de/controllers/tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ de:
task_found: 'In der externen App wurde eine entsprechende Aufgabe gefunden. Sie können: <ul><li><b>Überschreiben</b> Sie die Aufgabe in der externen App. Dadurch werden alle Änderungen, die in CodeHarbor vorgenommen wurden, auf die externe App übertragen.<br>Vorsicht: Dadurch werden alle potenziellen Änderungen in der externen App überschrieben. Dadurch wird die Aufgabe geändert (und möglicherweise zerstört), auch wenn sie derzeit in einem Kurs verwendet wird.</li><li><b>Erstellen Sie eine neue</b> Aufgabe, die den aktuellen Zustand dieser Aufgabe kopiert. Dadurch wird eine Kopie dieser Aufgabe in CodeHarbor erstellt, die dann als völlig neue Aufgabe in der externen App exportiert wird.</li></ul>'
task_found_no_right: 'In der externen App wurde eine entsprechende Aufgabe gefunden, aber Sie haben keine Rechte, sie zu bearbeiten. Sie können: <ul><li><b>Eine neue Aufgabe erstellen</b>, die den aktuellen Zustand dieser Aufgabe kopiert. Dadurch wird eine Kopie dieser Aufgabe in CodeHarbor erstellt, die dann als völlig neue Aufgabe in der externen App exportiert wird.</li></ul>'
gpt_generate_tests:
invalid_api_key: Der API-Schlüssel fehlt in Ihrem Profil oder ist ungültig. Bitte fügen Sie den entsprechenden API-Schlüssel in Ihrem Profil hinzu, um diese Funktion zu nutzen.
invalid_api_key: Der API-Schlüssel in Ihrem Profil ist ungültig. Bitte fügen Sie den entsprechenden API-Schlüssel in Ihrem Profil hinzu, um die KI-Funktionen zu nutzen.
invalid_description: Die angegebene Aufgabenbeschreibung stellt keine gültige Programmieraufgabe dar und kann daher nicht zum Generieren eines Unit-Tests genutzt werden. Bitte stellen Sie sicher, dass die Aufgabenbeschreibung eine klar formulierte Problemstellung enthält, die durch ein Programm gelöst werden kann.
no_language: Für diese Aufgabe ist keine Programmiersprache angegeben. Bitte geben Sie die Sprache an, bevor Sie fortfahren.
successful_generation: Unit-Test erfolgreich generiert. Bitte überprüfen Sie den generierten Test und vergeben Sie einen passenden Dateinamen.
1 change: 1 addition & 0 deletions config/locales/de/models.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ de:
avatar:
not_an_image: muss ein Bild sein
size_over_10_mb: Größe muss kleiner als 10MB sein
invalid_api_key: Der API-Schlüssel in Ihrem Profil ist ungültig. Bitte fügen Sie den entsprechenden API-Schlüssel in Ihrem Profil hinzu, um die KI-Funktionen zu nutzen.
models:
account_link:
one: Account-Link
Expand Down
5 changes: 3 additions & 2 deletions config/locales/de/views/users.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ de:
add_identity: Account mit %{kind} verknüpfen
cannot_remove_last_identity: Account-Verknüpfung zu %{kind} kann nicht entfernt werden, da weder ein Passwort gesetzt noch eine andere Identität verknüpft ist
manage_omniauth: Verknüpfte Accounts verwalten
openai_api_key_usage_html: Geben Sie hier Ihren OpenAI-API-Schlüssel ein, um verschiedene KI-Funktionen innerhalb der Plattform zu nutzen. Sie können einen API-Schlüssel auf der <a href='%{openai_api_link}' target='_blank' rel='noopener noreferrer'>OpenAI-Website</a> erstellen.
remove_identity: Account-Verknüpfung zu %{kind} entfernen
shared:
notification_modal:
Expand All @@ -51,9 +52,9 @@ de:
delete_modal:
title: Warnung
full_name: Vollständiger Name
not_provided_openai_key: Nicht eingegeben
openai_api_key: OpenAI API-Schlüssel
openai_api_key_not_provided: Nicht eingegeben
openai_api_key_provided: Eingegeben
private_information: Private Informationen
provided_openai_key: Eingegeben
public_information: Öffentliche Informationen
send_message: Nachricht senden
2 changes: 1 addition & 1 deletion config/locales/en/controllers/tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ en:
task_found: 'A corresponding task has been found on the external app. You can: <ul><li><b>Overwrite</b> the task on the external app. This will transfer all changes made on CodeHarbor to the external app.<br>Careful: This will overwrite all potential changes made on the external app. This will change (and might break) the task, even if it is currently in use by a course.</li><li><b>Create a new</b> task which copies the current state of this task. This will create a copy of this task on CodeHarbor, which will then be exported as a completely new exercise to the external app.</li></ul>'
task_found_no_right: 'A corresponding task has been found on external app, but you don''t have the rights to edit it. You can: <ul><li><b>Create a new</b> task which copies the current state of this task. This will create a copy of this task on CodeHarbor, which will then be exported as a completely new task to the external app.</li></ul>'
gpt_generate_tests:
invalid_api_key: The API key is missing from your profile or is invalid. Please add the appropriate API key in your profile to use this feature.
invalid_api_key: The API key in your profile is invalid. Please add the appropriate API key in your profile to use the AI features.
invalid_description: The task description provided does not represent a valid programming task and therefore cannot be used to generate a unit test. Please make sure that the task description contains a clearly formulated problem that can be solved by a program.
no_language: Programming language is not specified for this task. Please specify the language before proceeding.
successful_generation: Unit test generated successfully. Please check the generated test and assign an appropriate filename.
1 change: 1 addition & 0 deletions config/locales/en/models.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ en:
avatar:
not_an_image: needs to be an image
size_over_10_mb: size needs to be less than 10MB
invalid_api_key: The API key in your profile is invalid. Please add the appropriate API key in your profile to use the AI features.
models:
account_link:
one: Account link
Expand Down
5 changes: 3 additions & 2 deletions config/locales/en/views/users.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ en:
add_identity: Link %{kind} account
cannot_remove_last_identity: Cannot remove account link to %{kind} because neither a password is set nor another identity is linked
manage_omniauth: Manage linked accounts
openai_api_key_usage_html: Enter your OpenAI API key here to use various AI features within the platform. You can create an API key on the <a href='%{openai_api_link}' target='_blank' rel='noopener noreferrer'>OpenAI website</a>.
remove_identity: Remove account link to %{kind}
shared:
notification_modal:
Expand All @@ -51,9 +52,9 @@ en:
delete_modal:
title: Warning
full_name: Full name
not_provided_openai_key: Not entered
openai_api_key: OpenAI API Key
openai_api_key_not_provided: Not entered
openai_api_key_provided: Entered
private_information: Private Information
provided_openai_key: Entered
public_information: Public Information
send_message: Send Message
19 changes: 3 additions & 16 deletions spec/controllers/tasks_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# frozen_string_literal: true

require 'rails_helper'
require 'webmock/rspec'

RSpec.describe TasksController do
render_views
Expand All @@ -10,6 +9,7 @@
let(:collection) { create(:collection, users: [user], tasks: []) }
let(:valid_attributes) { {user:, access_level:} }
let(:access_level) { :private }

let(:invalid_attributes) { {title: ''} }

describe 'GET #index' do
Expand Down Expand Up @@ -1046,8 +1046,10 @@
let(:task_user) { create(:user, openai_api_key: 'valid_api_key') }
let(:access_level) { :public }
let(:task) { create(:task, user: task_user, access_level:) }
let(:mock_models) { instance_double(OpenAI::Models, list: {'data' => [{'id' => 'model-id'}]}) }

before do
allow(OpenAI::Client).to receive(:new).and_return(instance_double(OpenAI::Client, models: mock_models))
sign_in task_user
end

Expand Down Expand Up @@ -1099,20 +1101,5 @@
expect(flash[:alert]).to eq(I18n.t('tasks.task_service.gpt_generate_tests.invalid_description'))
end
end

context 'when GptGenerateTests raises InvalidApiKeyError' do
before do
allow(TaskService::GptGenerateTests).to receive(:call).and_raise(Gpt::InvalidApiKeyError)
post :generate_test, params: {id: task.id}
end

it 'redirects to the task show page' do
expect(response).to redirect_to(task_path(task))
end

it 'sets flash to the appropriate message' do
expect(flash[:alert]).to eq(I18n.t('tasks.task_service.gpt_generate_tests.invalid_api_key'))
end
end
end
end
47 changes: 47 additions & 0 deletions spec/models/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,53 @@
expect(user.errors[:avatar]).to include(I18n.t('activerecord.errors.models.user.attributes.avatar.size_over_10_mb'))
end
end

context 'when openai_api_key is present and valid' do
before do
allow(OpenAI::Client).to receive(:new).and_return(instance_double(OpenAI::Client, models: mock_models))
user.openai_api_key = 'valid_key'
end

let(:mock_models) { instance_double(OpenAI::Models, list: {'data' => [{'id' => 'model-id'}]}) }

it 'is valid' do
expect(user).to be_valid
end
end

context 'when openai_api_key is present and invalid' do
before do
allow(OpenAI::Client).to receive(:new).and_return(instance_double(OpenAI::Client, models: mock_models))
allow(mock_models).to receive(:list).and_raise(OpenAI::Error)
user.openai_api_key = 'invalid_key'
user.valid?
end

let(:mock_models) { instance_double(OpenAI::Models) }

it 'is not valid' do
expect(user).not_to be_valid
end

it 'adds an error for invalid api key' do
expect(user.errors[:base]).to include(I18n.t('activerecord.errors.models.user.invalid_api_key'))
end
end

context 'when openai_api_key remains the same' do
before do
allow(OpenAI::Client).to receive(:new).and_return(instance_double(OpenAI::Client, models: mock_models))
user.openai_api_key = 'same_key'
user.save!
user.valid?
end

let(:mock_models) { instance_double(OpenAI::Models, list: {'data' => [{'id' => 'model-id'}]}) }

it 'does not trigger API validation' do
expect(user.errors[:base]).not_to include(I18n.t('activerecord.errors.models.user.invalid_api_key'))
end
end
end

describe '#destroy' do
Expand Down
28 changes: 12 additions & 16 deletions spec/policies/task_policy_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,22 @@
context 'when user is admin' do
let(:user) { create(:admin, openai_api_key:) }

it { is_expected.to forbid_only_actions %i[generate_test] }
context 'without gpt access token' do
let(:openai_api_key) { nil }

context 'with gpt access token' do
let(:openai_api_key) { 'access_token' }

it { is_expected.to permit_all_actions }
it { is_expected.to forbid_actions(%i[generate_test]) }
it { is_expected.to permit_actions(generic_user_permissions) }
end
end

context 'when task is from user' do
let(:task_user) { user }

it { is_expected.to forbid_only_actions %i[generate_test] }

context 'with gpt access token' do
let(:openai_api_key) { 'access_token' }
context 'without gpt access token' do
let(:openai_api_key) { nil }

it { is_expected.to permit_all_actions }
it { is_expected.to forbid_actions(%i[generate_test]) }
it { is_expected.to permit_actions(generic_user_permissions + %i[edit update show export_external_start export_external_check export_external_confirm download add_to_collection duplicate]) }
end
end

Expand All @@ -77,13 +75,11 @@
context 'when user is group-admin' do
let(:role) { :admin }

it { is_expected.to permit_only_actions(group_member_permissions) }

context 'with gpt access token' do
let(:openai_api_key) { 'access_token' }
let(:group_member_permissions_with_generate_test) { group_member_permissions + %i[generate_test] }
context 'without gpt access token' do
let(:openai_api_key) { nil }

it { is_expected.to permit_only_actions(group_member_permissions_with_generate_test) }
it { is_expected.to forbid_actions(%i[generate_test]) }
it { is_expected.to permit_actions(group_member_permissions) }
end
end
end
Expand Down
1 change: 0 additions & 1 deletion spec/services/task_service/gpt_generate_tests_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
# frozen_string_literal: true

require 'rails_helper'
require 'webmock/rspec'

RSpec.describe TaskService::GptGenerateTests do
describe '.new' do
Expand Down

0 comments on commit e1e1d45

Please sign in to comment.