Quantcast
Channel: APEX-AT-WORK by Tobias Arnhold
Viewing all 177 articles
Browse latest View live

Enable save button on form change

$
0
0
Today I had the requirement that the save button should stay disabled until a form item changed.

After digging around I found a quite easy solution which worked well until now.

Save Button
Static ID: saveBtn
Custom Attributes: disabled

Dynamic Action
Event: Page Load
Execute Javascript Code:
$('#wwvFlowForm').on('input change', function() {
    $('#saveBtn').attr('disabled', false);
});



Simple but effective.

Copy and Paste to clipboard

$
0
0
Well I had the requirement to copy the content of a textarea into the clipboard. There are two ways to do that:

1. Build a dynamic action with custom Javascript code:
Copy Text to Clipboard

Code example - with dynamic action on "Click" and "Execute Javascript Code":
/* Select the text field */
$('#P1_APEX_ITEM').select();

/* Copy the text inside the text field */
document.execCommand("copy");



2. Use an APEX plugin:
Copy to Clipboard (v1.1) - build by Dick Dral



Icons made by Vitaly Gorbachev from www.flaticon.com is licensed by CC 3.0 BY

Set APEX application name for Dev, Test and Prod environment in the same database

$
0
0
In case you have a small application where development, test and maybe also production environment are on the same database and your applications in this environment distinguish only by the application IDs. To setup a custom application name based on the ID you could do like this:


We assume our application name is "Training room app" defined in the "Shared Components"> "User Interface Attributes"


To differentiate the environments I add a dynamic action "Page Load" on Page 0.
This dynamic action is executing custom Javascript code:

if ('&APP_ID.' == '200') {
  $('.t-Header-logo').find('span').html('Training room app - <b style="color:#008A34">Test Environment</b>');
}
else if ('&APP_ID.' == '300') {
  $('.t-Header-logo').find('span').html('Training room app - <b style="color:#9366a5">Development Environment</b>');
}


The code is changing the name of the logo area.

 

#nextGENTrip18 a Twitter story

$
0
0
I would like to give you a feeling about our #NextGEN community activities by showing you a collection of tweets at the latest event which we participated:

The #ApexDay2018 in Stockholm at the SWEOUG

We have been 12 IT enthusiast from different places and different life circumstances.
 - 6 women and 6 men
 - 5 students and 5 employees and 2 freelancer
 - 9 Oracle specialists and 7 of them working primary with APEX


Goal

Get everyone further engaged in #NextGEN activities and inspire new IT enthusiasts to join our community/activities/events.

Follow DOAG #NextGEN

Website: https://www.doag.org/de/nextgen/nextgen/
Twitter: https://twitter.com/doagnextgen
Instagram: https://www.instagram.com/doagnextgen

The Twitter story




























5 reasons why 








You came this far... That can only mean that you want to join us.
We have a Slack channel where we organize new events and help each other on different topics. Especially APEX newcomer get a lot of support. Slack channel communication is in German. If you want to join then just write us an email: nextgen@doag.org


Configure the SQL Developer on Mac OS X for AWS Cloud access

$
0
0
I have struggled a while now to correctly configure my Mac so that I can access an Oracle database in the AWS cloud.

Got a couple of strange connection errors:
I/O-Fehler: General SSLEngine problem
Handshake error

I even created a question in the forum:
https://community.oracle.com/message/14924079#14924079
At the end my friend Rüdiger helped me finding the right solution.

What was my configuration:
 - Max OS X 'El Captain'
 - JDK version: Build 1.8.0_181-b13
 - SQL Developer version:  Version 18.2.0.183
 - AWS cloud connectivity via SSL
 - Oracle database

Step by step:

1. Create the certificate "rds-ca-2015-root.der":
https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Appendix.Oracle.Options.SSL.html

2. Get your current JDK version:
java -version 
java version "1.8.0_181"

3. Copy the file CER in your JAVA_HOME/jre/lib/security
Full HOME directory: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home

4. Open a terminal window and go into that directory:
cd /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/security

5. Execute keytool
keytool -import -alias rds-root -keystore cacerts -file rds-ca-2015-root.der

6. Start your SQL Developer and create a new connection


Connection type: Advanced
JDBC-URL:
jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=tcps)(HOST=dbxe.xyz.server-center1.rds.amazonaws.com)(PORT=7575))(CONNECT_DATA=(SERVICE_NAME=DBXE)))

Flagge zeigen - IT gegen Rechts

$
0
0
Ich habe lange gezögert und überlegt… Es ist aber Zeit! Zeit mich zu positionieren, damit jedem klar ist, für welche Form von IT ich stehe. In Zeiten wie diesen ist es nicht mehr möglich, sich einfach weg zu ducken und zu hoffen!
Dazu möchte ich euch kurz etwas über die Geschichte meiner Familie erzählen:
Zwei meiner Urgroßväter haben im 2. Weltkrieg gekämpft - der eine als Freiheitskämpfer für den Kommunismus und der andere als Soldat für die Wehrmacht.

Das Ergebnis:
Tod, Leid und Vertreibung.

Und heute?
Ich selbst bin auch in einer gewissen Art und Weise geflüchtet… Geflüchtet von diesem sinnlosen Spruch "sei froh, dass du Arbeit hast". Seit ich im Westen von Deutschland lebe, habe ich viele Menschen kennengelernt und nur wenige sind mir mit "Anti-Ossi"-Sprüchen begegnet, dafür viele neue Farben, Namen und Lebenseinstellungen, die ich so bisher nicht kannte. Dieses sogenannte "Multi-Kulti" und die zumeist vorurteilsfreie / offene Gesellschaft war eine Bereicherung für meine eigene Entwicklung, sowohl im IT Bereich als auch im normalen Leben.

Ich hatte das Glück Menschen aus allen Teilen der Welt kennenlernen und schätzen lernen zu dürfen.

Die Vorstellung, dass Menschen in unserem Land zukünftig gejagt, vertrieben oder getötet werden, lässt mich innerlich kalt erschauern. Wer nach Chemnitz meint, dass diese Angst an den Haaren herbeigezogen sei, hat die Lage noch nicht erkannt.

Ich für meinen Teil stehe für eine IT des Miteinanders, des Austauschs und der Zusammenarbeit unabhängig von Hautfarbe, Herkunft, Religion, Geschlecht und sexueller Orientierung. Ich bewerte den einzelnen Menschen nach dessen Fähigkeiten, ob und wie ich mit diesen zusammenarbeiten kann und will.
Es ist jetzt Zeit, dass wir Zeichen setzen, egal ob als Einzelperson, Verein oder Firma.
Bald schon könnte die Angst unsere Entscheidungen zu sehr beeinflussen.

Daher möchte ich gern auf die "Pride in London" referenzieren, diese sollte ein Vorbild für unser zukünftiges Handeln auch im IT Umfeld darstellen.

Bild von der #pridelondon2018 - Oracle OPEN (LGBTQ+)
Denn leider ist es nicht mehr nur Ausgrenzung für die wir auf die Straße müssen, jetzt ist unsere Demokratie gefährdet.

Ps.:
Dieser Kommentar von spiegel.de trifft meine Sicht der Dinge sehr gut:
http://www.spiegel.de/politik/deutschland/afd-wer-sie-waehlt-waehlt-nazis-a-1226160.html

APEX client side error messages - apex.message.showErrors

$
0
0
Since APEX 5.1 we have the ability to create client side messages without the requirement to use a plugin or custom code to create nice looking client side messages.

For that requirement the APEX team created the apex.message library.

This library provides the same look and feel as if you would create a message out of your Page Designer > Validation Area.



In this example I will tell you something about a more advanced way to use the client side error messages.

As the documentation says this is the default way to create a client side error message:

/* General page error message */
apex.message.showErrors([
{
type: "error",
location: "page",
message: "Page error has occurred!",
unsafe: false
}
]);
  /* Item error message */ apex.message.showErrors([
{
type: "error",
location: [ "page", "inline" ],
pageItem: "P1_ITEM",
message: "Value is required!",
unsafe: false
}
]);

Example for a page error message:




In my example I want to check more then one item. For that I created an array including all item names I wanted to check. Within a "FOR" loop I run through every item and created an error message if the value was NULL. If no error occurred I run another custom dynamic action.

apex.message.clearErrors();

var chkErr = 0;
var arr = [
'P1_STRASSE',
'P1_HAUS_NR',
'P1_PLZ',
'P1_ORT',
'P1_ITEM_XYZ'
];

for (var i in arr) {
if ($v(arr[i]).length == 0) {
apex.message.showErrors([
{
type: apex.message.TYPE.ERROR,
location: ["inline"],
pageItem: arr[i],
message: "
Value is required!",
unsafe: false
}
]);
chkErr = 1;
}
}

if ( chkErr == 0 ) { 
/* Custom dynamic action call when no error occurred */
apex.event.trigger(document, 'customDA', [{customAttribute:'1'}]);
}
You want to know more? Check out the documentation and this series of blog posts by Martin D'Souza:
https://www.talkapex.com/2018/03/custom-apex-notification-messages/
https://www.talkapex.com/2018/03/how-to-save-page-data-but-show-errors-in-apex/

Eine verrückte Zeit endet...

$
0
0
Hallo zusammen,

ich habe leider eine schlechte Nachricht zu verkünden. Ich trete als Leitungskraft innerhalb der DOAG NextGen-Community zurück. Das kommt leider nicht nur für euch überraschend, und glaubt mir, dieser Schritt ist mir nicht leichtgefallen, weil ich weiß, dass ihr da draußen auf das, was die NextGen geleistet hat, vertraut und mit Freude die Entwicklung und den Spirit mitverfolgt habt.

Leider gab es vor kurzem mehrere Vorstandsbeschlüsse, die ein weiteres Engagement für mich praktisch unmöglich machen. Diese Beschlüsse betreffen die grundsätzliche Ausrichtung der NextGen-Community innerhalb der DOAG.

Beschluss 1:
„Die #NextGen-Community soll nur Themen behandeln, die NICHT bereits durch Themenverantwortliche in anderen Communitys abgedeckt werden.“

Heißt, die Technologie APEX darf nicht mehr mit der NextGen in Verbindung gebracht werden.

Beschluss 2:
„Für die Nachwuchsförderung bei bestehenden Themen sollen die jeweiligen Themenverantwortlichen der jeweiligen Communitys verantwortlich sein, nicht die #NextGen. Eine Zusammenarbeit und Einbindung in die #NextGen kann bestehen bleiben.“

Heißt für die Technologie APEX, dass Niels aus der Development-Community für APEX für den Nachwuchs verantwortlich ist.

Was macht die NextGen dann zukünftig?
Die DOAG verfolgt das Ziel, sich neuen, auch Oracle-unabhängigen Technologien zu öffnen und genau hier soll, denke ich, die NextGen in Zukunft der Treiber sein. Weitere Details dazu dann von der NextGen auf der DOAG K+A.

Was bedeutet das für die APEX-Community?
Ganz ehrlich, zunächst einmal einen großen Rückschlag. Die Leute, die im APEX Umfeld sind und gleichzeitig das gemocht haben, was die NextGen bisher verkörpert hat, diesen jungen Tech Enthusiasts muss die DOAG Development-Community nun schnell und einfach den Einstieg in deren Community bereiten, ansonsten werden diese Leute abspringen und sich anderweitig engagieren. Ich rede hier nicht von mir, sondern von denen die ich auf meiner Reise getroffen und schätzen gelernt habe:
Jonas, Charlotte, Pierre, Caro, Matthias, Jan, Davide, Ali, Philipp, Felix, Louisa, Rebecca, Maximilian, Victoria, Sebastian, Steven, Christopher, Abby, Florian, um nur einige zu nennen.

Und meine Zukunft?
Ich werde meinen Spirit und meinem Community-Fokus beibehalten und über andere Wege das Besondere tun um diese einzigartige Community zu bereichern.

Das Ergebnis werdet ihr auf der APEX Connect 2019 sehen, weil ich ab der APEX Connect 2019 für den Track APEX die Verantwortung innehabe. Und hier gibt es natürlich nur ein Ziel: den Community-Spirit an die Spitze zu treiben und mit möglichst vielen APEX-Enthusiasten eine unvergessliche Konferenz auf die Beine zu stellen.

Außerdem unterstütze ich das FABE Projekt (forallabeautiful.earth) von Steven Feuerstein und Vincent Morneau aus Sicht der Datenmodellierung.

Der NextGen-Community selbst kann ich nur alles Gute und viel Erfolg auf dem neuen Weg wünschen. Mir werden diese besonderen Momente immer im Herzen erhalten bleiben und ich bin sehr dankbar dafür solch einen Community-Spirit erlebt haben zu dürfen. Menschen wie Carolin Hagemann, Davide Groppuso, Jonas Gassenmeyer, Matthias Nöll und viele Andere die dem Wort "Community" den Spirit eingehaucht haben den ich geliebt habe.

Danke für eine GEILE Zeit! Und auch Danke Ingo Sobik für das unterstützen von verrückten Aktionen und dem ganzen DOAG Office diese Aktionen dann am Ende gemeinsam auch in die Tat umgesetzt zu haben.

Never forget: #POUGTrip, #nextGENrocks #NextGENTrip18 #Roadshow # CommunitySpirit





Interactive Grid - Column stretching

$
0
0
Since APEX 18.1 you can define the stretch behavior per column.

You can choose between 3 different options:

Use Report Setting
The column will use the Stretch Report Setting set by the End User to define if the column should stretch or not.
In APEX 18.1 you have a new option to let the end user decide if the columns should be stretched or not.
This option effects all columns by default.

Never
The column will not stretch and always just use the width specified. This is useful for columns with short content like Yes/No or Numbers.
This option will over rule "Use Report Setting" and is really good for link columns or as the documentation says: short content columns.

Always
The column will always stretch, irrespective of the Stretch Report Setting set by the End User.


Results can look slightly different:


Imho: Small enhancement with hugh impact.

Modal Dialogs Part 1 - Intro

$
0
0
In this 5 part series of blog posts I will share my understanding of how you can open modal dialogs with dynamic values.

First of all: Why would you need that?
There are particular business cases where your modal dialog (page 2) uses data from the parent page (page 1) which was created during runtime.
For example: Map Selector
I want to select a certain position from a map. The map is called by a button and displayed via a modal dialog. The parameter for the default position are defined on the parent page. After I have chosen a particular position on the map the Latitude and Longitude parameter should be transmitted back towards the parent page.


In this introduction I will explain how you can do it with "Standard APEX" functionality.
On my main page 10 I have several search items (like City, Street...) , two return items (Lat, Lon) and a button called SEARCH. The button executes a standard "Submit Page".
In the processing area I add a branch "After Submit" which gets executed by the SEARCH button. The branch process will open page 11 and will transmit all search parameters.
Info: This way opening a modal dialog may not work below APEX 18.1


Now the modal dialog will open. On page load a dynamic action gets executed to create the map:

vAddress =
  apex.item('P11_CITY').getValue() + '' +
  apex.item('P11_POSTAL_CODE').getValue() + '' +
  apex.item('P11_STREET').getValue() + '' +
  apex.item('P11_HOUSE_NUMBER').getValue()

vDiameter = apex.item("P11_DIAMETER" ).getValue();

apex.event.trigger(document, 'showOpenLayersMap', [{address: vAddress, diameter:vDiameter}]);void(0);


In that little code snippet above I create a single string "address" and a diameter parameter to pass into the map function. Which is by the way created on page 0 as a custom dynamic action named "showOpenLayersMap".

My showOpenLayersMap function is doing all the magic. In the moment the end user is doing a double click on a certain position inside the map a trigger executes a custom dynamic action on page 11 named "setLatLon".

...
apex.event.trigger(
  document, 
  'setLatLon', 
  [{latitude:coordinatesItems[1], longitude:coordinatesItems[0]}]
);
...



This one fires 3 TRUE actions to return the Latitude and Longitude parameter to page 10:
1. Execute JavaScript Code
apex.item("P11_LON" ).setValue(this.data.longitude);
apex.item("P11_LAT" ).setValue(this.data.latitude);

2. Execute PL/SQL Code
:P10_LON := :P11_LON;
:P10_LAT := :P11_LAT;

3. Execute JavaScript Code
parent.location.reload();

Unfortunately I couldn't find a way to send the items back to the parent page dynamically. I just couldn't react on the "Dialog Closed" event. I think it has to do with the explicit submit. The dynamic action handler has no idea about the modal dialog.



Ps.: A separate blog post about open layers and open street map will come later...

Der Fahrplan bis zur APEX Connect 2019

$
0
0
Im Mai 2017 hatte ich euch gefragt worauf ihr euch am meisten freut was die damals anstehende APEX Connect betraf.
Nun schreiben wir das Jahr 2019 und bis zur Connect stehen noch einige Events vor der Tür, bei denen es neben dem technischen Wissensaustausch vor allem auch um die Vernetzung und das teilen des gemeinsamen Spirits geht.


Daher hier der Fahrplan bis zur APEX Connect 2019

Meetups
Das nächste Meetup findet am 11.03. in München zum Thema "APEX 19.1 New Features" statt.
Die anderen Meetupgruppen befinden sich noch im Winterschlaf. News gibt es aus Dresden, dort wurde eine neue Meetup Gruppe gegründet. Das erste Treffen ist bereits in Planung.
Eine Übersicht aller Meetups findet ihr auf apex.world.
Eure Stadt fehlt? Ihr wollt aber trotzdem eine Gruppe gründen und wisst nicht wie? Schreibt mich einfach an (Twitter, Mail) und ich versuche euch dabei zu unterstützen.


Workshops
Am 18.03. findet ein kostenloser APEX Workshop in Trier statt inklusive Hands On. Beeilt euch die Plätze sind begrenzt.

In Frankfurt gibt es am 13.03 einen kostenlosen Oracle Datenbank-Technologie Tag.

Zwischen dem 09.-10.04 bietet die MT AG einen APEX Migrations-Workshop an. Geeignet für alle die endlich auf APEX 18.2 migrieren möchten.


APEX Competition
Bis zur Connect läuft parallel auch die APEX Competition 2019 bei der es darum geht ein APEX Plugin zu entwickeln. Der Gewinner erhält 850 €.
Die Competition läuft noch bis zum 07. April. Genug Zeit für eurer erstes Plugin, den passenden Einstieg findet ihr übrigens in diesem aufgezeichneten Webinar:
Entwicklung von Plug-ins für Oracle Apex – Kurzer Einstieg


Im Mai findet dann die heiligste aller APEX Veranstaltungen im deutschsprachigen Raum statt:

Die APEX Connect

Am Vortag der Connect dem 06.05. (Montag) wird es ein kostenloses Meetup geben, bei dem bekannte Oracle Core Entwickler die letzten News rund um APEX, PL/SQL und Javascript teilen werden.
Zwischen dem 07.05. und dem 09.05. dreht sich dann alles nur noch um die bekannten 4 Buchstaben 

A..P..E..X..

Highlights rund um das Event werden in den kommenden Wochen über unterschiedliche Blogposts detaillierter kommuniziert, bis dahin schaut euch doch mal die Vortragshighlights und den Bewertungsprozess näher an. Das komplette Programm findet ihr hier:

Mein Highlight dieses Jahr sind definitiv die Beginner Sessions:
Geeignet für Newcomer, Studenten, ehemalige Forms-Entwickler, DBA's und alle die APEX näher kennenlernen möchten.

And the APEX Coin goes to...

$
0
0
And the APEX Coin goes to...



Before I tell you.. Here is the story behind it!

Adrian Png is a Senior APEX developer and he is doing a lot of things for the APEX Community. For that reason he earned the APEX.WORLD Member of the year award.

His last outstanding idea was the APEX Challenge Coin!
In short: You can announce one or more great APEX developers who you think earn an APEX Coin.
But please read the full story here: Announcing the APEX Challenge Coin 


Of course I had to ask Adrian to send me 2 coins.


Unfortunately I know to many great APEX developers only in Germany. How in hell will I ever be able to choose one???

So my choice fell on 2 passionate people doing not only APEX. They lead and support our German APEX Community since years. Here are some examples:
 - APEX Meetup organizers
 - Member of the DOAG development community
 - Supporting the APEX Connect conference

But this is not the main reason!

The main reason is the passionate work in supporting and helping "Woman in Tech" (WIT) to become part of our wonderful APEX community.


And the APEX Coin goes to... Carolin Hagemann and Sabine Heimsath !






P.S.: As you can see on the picture I got 3 coins. So thanks Adrian for giving me one as well. :)

Die inoffizielle APEX Connect Agenda

$
0
0
Noch etwas mehr als einen Monat bis zur APEX Connect 2019 und so langsam steigt bei allen die Vorfreude. Dieses Jahr wird es neben dem voll gepackten offiziellen Programmplan auch die ein oder andere Community Aktivität zu erleben geben.

Aber eins nach dem Anderen. Zunächst mal der Verweis auf das aktuelle Vortragsprogramm:


Am Dienstag den 07.05. ist um 9.00 Uhr der offizielle Startschuss und am 09.05. um 17:00 Uhr beenden wir die Connect mit einer Keynote von Jonathan Lewis.

Davor und dazwischen gibt es aber neben dem offiziellen Programm noch allerhand MEHR zu erleben.

Montag 17-21 Uhr: Meetup in der Uni Bonn (kostenlos)
Montag ab 21 Uhr: Gemeinsames Abendessen + 🍺; Ort - offen - (Selbstzahler)

Dienstag 18-20 Uhr: individuelles Abendbrot in der nähe des Hotels
Dienstag ab 20:30 Uhr: Offene Fragerunde mit dem Oracle Development Team und das Beste aus den letzten Monaten.
Dienstag ab 22 Uhr: Ausklingen an der Bar (Selbstzahler)

Mittwoch 7:30 Uhr: 5k Fun Run
Mittwoch 18:00 Uhr: Cruise Dinner
Mittwoch ab 22 Uhr: Party Open End im Club "N8schicht" (Selbstzahler)

Damit ihr alles Wichtige auch während der Konferenz mit bekommt, meldet euch an Twitter an und folgt diesen beiden Hashtags:
#orclapex #apexconn19


Interactive Grid: After Update Trigger

$
0
0
If you want to run a process after the "Interactive Grid" successfully updated all rows you can achieve this with a dynamic action. This can be necessary if you need to update certain columns calculated over several row in the same table you updated within the grid. Problem is to refresh the related data inside the grid as well.

Example:
You edit 3 rows for column A within your grid.
The grid updates column A row by row.
After that an update process should calculate a new result for column B which includes the data from all those updated 3 rows in column A.

Column A
row 1: 150 
row 2: 200
row 3: 100

Column B includes the sum of  column A
row 1: 450 
row 2: 450 
row 3: 450  

All you need to do is to define your Grid like this:
Interactive Grid > Advanced > Static ID: igYourGrid

Dynamic Action Event: Save [Interactive Grid]
Selection Type: Region
Region: Your Grid
Event Scope: Static

Action: Execute PL/SQL Code
custom_pkg.after_update_process;

Action: Execute Javascript Code
var model = apex.region("igYourGrid").widget().interactiveGrid("getViews").grid.model;
model.fetchRecords(model._data);




Be aware that the custom_pkg.after_update_process; process may not be executed when JavaScript errors occur.

This solution is completely dynamic and does not require a page submit.

--
--

Tip of the day
Since all conferences get cancelled worldwide join those online conferences instead:
ACE's at Home: https://itoug.it/aces-at-home/ (March 31th '20)
APEX@Home: https://asktom.oracle.com/pls/apex/f?p=100:551:::NO:551:P551_CLASS_ID:744 (April 16th '20)

Interactive Grid: Validation - Check for duplicated column entries over all rows

$
0
0
I had this situation now a few times and was always to lazy to write it down. :/

During my last task within the fabe project I hat to create a validation to check for duplicated entries inside an Interactive Grid.
Whenever I add "None of the above" twice, an error should occur:
This blog post from Lino Schilde was a good start for my final solution:

Interactive Grid Validation
Validation of Type: PL/SQL Function (returning Error Text)
Code:
declare
  v_cnt number;
begin
 
-- check only if insert or update (not delete "D")
if :APEX$ROW_STATUS in ('C','U') then

  -- select only if the current row is set to Y
  -- positive result if one answer was set to Y
  select max(case when none_yn = 'Y' then 1 else 0 end)
  into v_cnt
  from answer
  where question_id = :P301_QUESTION_ID
  and answer_id != nvl(:ANSWER_ID,0)
  and :NONE_YN = 'Y';
 
  if v_cnt = 1 then
    return 'Another answer was already set up with "None of the above". You need to change and save it first.';
  end if;

end if;
end;

My solution used a "max" aggregation within a "case when" trick to get the right result.

--
--

Tip of the day
All those meeting clients (Skype, Zoom, Hangouts ...) can also be used for APEX friends and beer meetups. #beer #orclapex


utPLSQL - Example Package

$
0
0
A few months ago I got an awesome task to do inside the fabe project:
"Create an utPLSQL test package for the app authentication package"

Well, I never used utPLSQL before and (due to some hangovers at conferences 🥴) I could never really get a heads up towards that topic. Luckily I know one of the main developers Samuel Nitsche. He claims to be a Sith Lord, but for me, he is a true Jedi.
Anyway, I used the force to get him to help me getting a foot into that topic.

First, he gave me some resources.

Documentation:
http://utplsql.org/utPLSQL/latest/

My personal favorite was this article he wrote for the DOAG magazine:
https://cleandatabase.wordpress.com/2019/12/18/but-that-worked-yesterday-or-why-you-should-test-with-utplsql/
(German version: https://www.doag.org/formes/pubfiles/11888853/06_2019-Red_Stack-Samuel_Nitsche-Aber_das_hat_gestern_noch_funktioniert_Testing_mit_utPLSQL.pdf)

A slide that explains the basic usage:


After a little private introduction I made my first attempt and had a discussion with the other Fabe team members about the code usage and naming rules. Well naming rules sounds harsh and we had a long discussion on the topic. Why? Because it is a damn important thing with several long-term consequences. Finally we as a team decided to do it like this:

Test package name should be the same as the original package with an additional "test_" prefix.
For the AUTHENTICATION_PKG the corresponding utPLSQL package was called: TEST_AUTHENTICATION_PKG.

More important were the names for the procedures:
They should be as descriptive as possible. Luckily, fabe runs on  Oracle 19c and we don't have  a 30 character limitation.
For example:
  try_to_hijack_another_users_automatic_login
  send_a_mail_with_the_request_to_reset_the_password

The big advantage - compared to having test methods named after the test methods - is that you focus on the task / behavior and you can have as many test cases for one single procedure as necessary.

So lets get to the code..

The package header looked like this:
Info: This code piece does not include the complete test case for my package.
create or replace package test_authentication_pkg is
-- %suite(Testing authentication logic)
-- %suitepath(Authentication)
-- %rollback(manual)

-- run code:
-- select * from table(ut.run('test_authentication_pkg'));

-- %beforeall
procedure generate_a_valid_apex_session;

-- %beforeeach
procedure create_a_test_user_in_user_profile;

-- %context(Running side programms)
-- %name(1_simple_procedures)

-- %test(generate a hash value and check the output)
procedure generate_a_hash_value_and_check_the_output;

-- %test(check the email format of the test_user)
procedure check_email_format_of_the_test_user;

-- %endcontext

-- %context(Authentication checks)
-- %name(2_login_procedures)

-- %test(verify a positive authentication)
procedure verify_a_positive_authentication;

-- %test(verify an authentication failure)
procedure verify_an_authentication_failure;

-- %test(generate a user login token)
procedure generate_a_user_login_token;

-- %endcontext

-- %aftereach
procedure remove_test_user;

end test_authentication_pkg;

I used a beforeall as well as a beforeeach process to setup my test case.
By using those annotations the procedures run automatically before the test cases (once / always).

The beforeall process created a valid APEX session which required a commit inside my test procedure. For that fact I had to manually clean up all test content and I had to use %rollback(manual)to get the package running.

I also used %context to be able to better organize my procedures.

Be aware that utPLSQL runs procedures not in a certain way as you defined it. That is the reason why I always recreate the test user and cleanup all data after each test procedure.
utPLSQL is intended to work like that. To check each test case independent from another one.

My package body looked like this.
create or replace package body test_authentication_pkg is
-- global variables
gc_user_id constant number := -1;
gc_app_user constant varchar2(100) := 'uttest.authentication_pkg@fab.earth';
gc_salt constant varchar2(100) := 'XYZ1235';
gc_username constant varchar2(100) := 'uttest';
gc_password constant varchar2(100) := 'ThisIsAValidTest';
gc_password_hash constant varchar2(100) := 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';

gc_creation_date constant date := to_date(to_char(localtimestamp,'dd.mm.yyyy') || ' 00:01', 'dd.mm.yyyy hh24:mi');


--
-- startup once before all procedures
--
procedure generate_a_valid_apex_session as
l_user_id_in_session_state number;
begin
remove_test_user;

apex_session.create_session(
p_app_id => 100,
p_page_id => 1,
p_username => gc_app_user
);

APEX_UTIL.SET_SESSION_STATE (
P_NAME => 'G_USER_ID',
P_VALUE => gc_user_id
);

l_user_id_in_session_state := APEX_UTIL.GET_SESSION_STATE (
p_item => 'G_USER_ID'
);

--check
ut.expect(l_user_id_in_session_state)
.to_equal(gc_user_id);

end generate_a_valid_apex_session;


--
-- preparation: insert into user_table
--
procedure create_a_test_user_in_user_table as

-- populate expected
l_user_id_entries number;

begin

INSERT INTO user_table(
user_id,
app_user,
active,
username,
first_name,
last_name,
bio,
timezone_id,
country_id,
salt,
password
)VALUES(
gc_user_id,
gc_app_user,
'Y',
gc_username,
'UT',
'Test',
'Internal test account which should not exist regularly.',
168, -- Berlin
86, -- Germany
gc_salt,
gc_password_hash --'ThisIsAValidTest'
);

select count(*)
into l_user_id_entries
from user_table
where user_id = gc_user_id;

--check
ut.expect(l_user_id_entries).to_equal(1);

end create_a_test_user_in_user_table;

--
-- cleanup from user_table
--
procedure remove_test_user as

-- populate expected
l_user_id_entries number;

begin

delete from user_pw_table
where email_address = gc_app_user;

delete
from user_log_table
where user_id = gc_user_id;

delete
from user_table
where user_id = gc_user_id;

end remove_test_user;


--
--
-- Simple procedure checks
--
--

PROCEDURE generate_a_hash_value_and_check_the_output IS
BEGIN
ut.expect(authentication_pkg.cust_hash(gc_app_user,gc_salt,gc_password))
.to_equal(gc_password_hash);
END;


PROCEDURE check_email_format_the_test_user IS
BEGIN
ut.expect(authentication_pkg.email_format(p_email => gc_app_user))
.to_be_true();
END;


--
--
-- Login tests
--
--

PROCEDURE verify_a_positive_authentication IS
BEGIN
ut.expect(authentication_pkg.custom_authenticate(gc_app_user,gc_password))
.to_be_true();
END;

PROCEDURE verify_an_authentication_failure IS
BEGIN
ut.expect(authentication_pkg.custom_authenticate(gc_app_user,gc_password||'X'))
.to_be_false();
END;


-- Create an user login entry / token and create JWT SSO Token as cookie
PROCEDURE generate_a_user_login_token IS
l_actual number;

l_owautil_var owa.vc_arr;
l_owautil_val owa.vc_arr;
BEGIN

-- Pre Test configuration
-- intialize owautil for the generate_fingerprint procedure
-- to prevent ORA-06502, ORA-06512: at "SYS.OWA_UTIL", line 354
l_owautil_var(1) := 'HTTP_USER_AGENT';
l_owautil_val(1) := 'Windows 10 Client with Firefox xxx.x';
owa.init_cgi_env( l_owautil_var.count, l_owautil_var, l_owautil_val );

-- start test case
authentication_pkg.generate_user_log (
p_user_id => gc_user_id
);

-- verifiy user_log_table entry
select count(*)
into l_actual
from user_log_table
where user_id = gc_user_id;

ut.expect(l_actual).to_equal(1);

-- remove test data
delete from user_log_table
where user_id = gc_user_id;

END generate_a_user_login_token;

end test_authentication_pkg;

Now there is a lot to say about that package body let me start with the obvious.

1. I used global variables for all of the values my  test case procedures rely on.
Advantage: Saves time when I need to adjust the parameters. It is also more readable by using understandable variable names.

2. Use the API and keep the test case simple
Use the predefined API procedures.
ut.expect(...).to_be_true();
ut.expect(...).to_equal(1);
ut.expect(...).to_equal(gc_user_id);

A common way writing the test cases are looking similar to this example:
procedure test_action as
l_actual number;
-- populate expected
l_expected number := 1;

begin
l_actual := authentication_pkg.check_something(gc_app_user);
ut.expect(l_actual).to_equal(l_expected);
end test_action;
I would recommend to do it like this:
procedure test_action as
begin
ut.expect(authentication_pkg.check_something(gc_app_user))
.to_equal(1);
end test_action;

Advantage:
Use the proper variable names instead of generic ones.
By splitting the procedure call and the result check in two lines. We can read it much better.
By adding the target procedure inside ut_expect, we spare an additional variable, create almost zero code and still keep the test readable.
Thanks to Samuel for showing me to code like this.

4. Use a custom APEX session procedure
Advantage: Independent code pieces like this should be extracted into separate procedures (separation of concerns). In that case you can focus on the real test case errors.

5.  Define your cleanup process
In my example remove_test_user I manually removed all data because in the original package I had some commits inside my called procedures. If you don't have that situation, instead you can  define a simple rollback process.
procedure remove_test_user as

begin
rollback;

end remove_test_user;

6. Error ORA-06502, ORA-06512: at "SYS.OWA_UTIL"
      l_owautil_var(1) := 'HTTP_USER_AGENT';
      l_owautil_val(1) := 'Windows 10 Client with Firefox xxx.x';     
      owa.init_cgi_env( l_owautil_var.count, l_owautil_var, l_owautil_val );

Why? You need to pretend to be a client. :) Otherwise it will be null.

The result for the real procedure looks like this:
authentication
  Testing authentication logic
    Password checks
      reset the test user password [,041 sec]
      send a mail with the request to reset the password [,107 sec]
      verify the requested password activity [,025 sec]
      verify the signup password [,019 sec]
    Authentication checks
      verify a positive authentication [,055 sec]
      verify an authentication failure [,069 sec]
      generate a user login token [,084 sec]
      automatically recreate an apex session for the test user [,26 sec]
      try to hijack another users automatic login [,238 sec]
    Running side programms
      generate a hash value and check the output [,053 sec]
      check the email format from the test_user [,036 sec]

Finished in 1,076257 seconds
11 tests, 0 failed, 0 errored, 0 disabled, 0 warning(s)



Thanks again Samuel, Hayden and the whole Fabe team making this blog post possible.
It showed me once more what community spirit really meant.


APEX CONNECT 2020 [ONLINE]

$
0
0
You may already noticed..
Anyway I'm glad to remind you that next week a full two day APEX online conference will be held:

APEX CONNECT 2020 [ONLINE]


We will have three parallel tracks divided in different topics and languages:
  • Track 1: APEX [🇩🇪]
  • Track 2: PL/SQL [🇩🇪]
  • Track 3: APEX and PL/SQL [🇬🇧]
Register now: https://apex.doag.org/en/home#c40443

We also plan a beer session on Tuesday evening (CET) so stay tuned on Twitter: #apexconn20

This is not all..
On Monday there will be an ACEs at Home APEX day including a nice presentation from my side:
Fighting climate change with Oracle - a worldwide initiative


Viewing all 177 articles
Browse latest View live