Montag, 3. April 2017

Calculate your working hours

The following query calculates your working hours by condensing the APEX_WORKSPACE_ACTIVITY_LOG.:

 
select distinct APEX_USER,
        TO_CHAR(MIN_DATE, 'Day DD.Mon.') START_DATE,
        TO_CHAR(MIN_DATE, 'HH24:MI') START_TIME,
        TO_CHAR(MAX_DATE, 'HH24:MI') STOP_TIME,
        EXTRACT(HOUR FROM WORK_TIME_PERIOD) || ':' ||
        LTRIM(TO_CHAR(ROUND(
          EXTRACT(MINUTE FROM WORK_TIME_PERIOD)
        + EXTRACT(SECOND FROM WORK_TIME_PERIOD) / 60, 0), '09'))
        WORKING_HOURS,
        EXTRACT(HOUR FROM BREAK_TIME_PERIOD) || ':' ||
        LTRIM(TO_CHAR(ROUND(
          EXTRACT(MINUTE FROM BREAK_TIME_PERIOD)
        + EXTRACT(SECOND FROM BREAK_TIME_PERIOD) / 60, 0), '09'))
        BREAK_HOURS,
        APPLICATION_NAME, PAGE_NAME, EVENT_CNT, GID
from (
    select distinct APEX_USER, MIN_DATE, MAX_DATE,
        ((MAX_DATE - MIN_DATE) DAY(9) TO SECOND) WORK_TIME_PERIOD,
        ((LEAD(MIN_DATE) OVER (PARTITION BY APEX_USER ORDER BY MIN_DATE ASC)
        - MAX_DATE) DAY(9) TO SECOND) BREAK_TIME_PERIOD,
        APPLICATION_NAME, PAGE_NAME, EVENT_CNT, GID
    from (
        select distinct APEX_USER, GID,
                MIN(VIEW_DATE) OVER (PARTITION BY APEX_USER, GID) MIN_DATE,
                MAX(VIEW_DATE) OVER (PARTITION BY APEX_USER, GID) MAX_DATE,
                COUNT(VIEW_DATE) OVER (PARTITION BY APEX_USER, GID) EVENT_CNT,
                FIRST_VALUE(APPLICATION_NAME) OVER(PARTITION BY APEX_USER, GID ORDER BY VIEW_DATE DESC) APPLICATION_NAME,
                FIRST_VALUE(PAGE_NAME) OVER(PARTITION BY APEX_USER, GID ORDER BY VIEW_DATE DESC) PAGE_NAME
        from (
            select APEX_USER, APPLICATION_NAME, PAGE_NAME, VIEW_DATE,
                SUM(GAP) OVER (PARTITION BY APEX_USER ORDER BY VIEW_DATE ASC) GID
            from (
                select APEX_USER, APPLICATION_NAME, PAGE_NAME, VIEW_DATE,
                    CASE WHEN VIEW_DATE - FIRST_VALUE(VIEW_DATE) OVER(PARTITION BY APEX_USER
                                            ORDER BY VIEW_DATE ASC ROWS BETWEEN 1 PRECEDING AND CURRENT ROW)
                                            < 1 / 1440 * 20 THEN 0 ELSE 1 END AS GAP
                from APEX_WORKSPACE_ACTIVITY_LOG
             )
        )
    )
)
where APEX_USER = V('APP_USER')
and MIN_DATE > SYSDATE - 3
order by GID;

Mittwoch, 22. März 2017

Managing Gigabytes of files

Vor einiger Zeit hatte ich ein Apex Projekt für die Verteilung von Dokumenten und Fotos.
Dabei sollten die Dokumente in einer Ordner Hierarchie angezeigt und zum Download bereitgestellt werden. Es sollten dann über 3GB an Dateien veröffentlicht werden. Da bot es sich an Zip-Archive für den Upload und Download zu verwenden.
Die open source library as_zip enthielt die dafür benötigten Funktionen.
https://technology.amis.nl/2010/06/09/parsing-a-microsoft-word-docx-and-unzip-zipfiles-with-plsql/

Bei Auslesen größerer Archive mit tausenden Dateien mit der Funktion as_zip.get_file versagte jedoch die Performance. Die Ursache war schnell gefunden: Die Funktion macht eine sequenziellen Scann durch das Archiv bis der übergebene Datei Pfadname gefunden wird. Vor dem Vergleich muss der binäre Inhalt auch noch nach Unicode konvertiert werden.
Es wird also ein Index für das Archiv benötigt, damit die Funktion as_zip.get_file direkt zugreifen kann. Nachdem ich mit der Prozedur as_zip.get_file_date_list schon eine Datumsliste aus dem Verzeichnis erstellt hatte, war es nicht schwer auch den Datei-Offset für jede enthaltene Datei in einem Array zu kalkulieren. Damit konnte as_zip.get_file nun richtig loslegen und große Archive in proportionaler Zeit zur Dateigröße einlesen. 

Um den Prozess weiter zu beschleunigen, habe ich die library unzip_parallel entwickelt. Diese library liest ein Zip-Archiv aus einer Sql-Tabelle und schreibt alle enthaltenen Dateien in zwei Tabellen.
Ein für die Dateien and die zweite für die Ordner Struktur als rekursive parent - child Relation.
(So kann die Ordner Struktur einfache in einer Tree-Region dargestellt werden.)
Für größere Archive verwendet die library das oracle package dbms_parallel_execute. 
Damit lies sich die Verarbeitungszeit noch einmal halbieren.

Die Funktionalität dieser library habe ich nun als Apex Plug-in veröffentlicht:

Process plug-in for reading a zip file from a table, storing all expanded files 
in one table and the folders for the files in a second table.
The table for the files has a least the two columns for file_name varchar2, file_content blob
and optionally file_date date, file_size number, mime_type varchar2(300), folder_id number.
The table for the folders has at least a folder_id number, parent_id number, folder_name varchar2.
When no folder definition is provided in the Folder Query attribute, full pathnames are stored in the file_name field of the files table.
Zip file larger than 5MB will be processed in parallel to reduce the processing time when parallel execution is enabled.

A demo of the plugin can be found here:

Can I have the Last-Modified Date - for that files?

In einem meiner letzten Projekte für Gebäude Management wurden Rechnungen und andere Dokumente gescannt und in die Datenbank geladen. Für die Geschäftsleute ist das Dokumenten-Datum neben dem Namen sehr wichtig. In der Apex 4 Umgebung gab es dafür keine vollständige Unterstützung. So wurde das Datum dann manuell eingegeben.

Das wollte ich ändern:

1. Beim Upload einer Datei in einer Webanwendung kann das letzte Änderungsdatum mit Javascript ausgelesen und so in ein Input Feld neben dem Namen übertragen werden.

function GetFileNameDate(p_FileItem, pNameItem, pDateItem) {
 $x(pNameItem).value = $x(p_FileItem).value;
 var input = $x(p_FileItem);
 if (input && input.files && input.files[0]) {
  var fdate = input.files[0].lastModifiedDate;
  var day = fdate.getDate();
  var mon = fdate.getMonth() + 1;
  var year = fdate.getFullYear();
  var hour = fdate.getHours();
  var min = fdate.getMinutes();
 
  if (day < 10) { day = '0' + day; }
  if (mon < 10) { mon = '0' + mon; }
  if (year < 10) { year = '200' + year; }
  else if (year < 100) { year = '20' + year; }
  if (hour < 10) { hour = '0' + hour; }
  if (min < 10) { min = '0' + min; }
 
  var datestr = day + "." + mon + "." + year + " " + hour + ":" + min;
  $x(pDateItem).value = datestr;
 }
}

2. Beim Download über einen normalen Link kann nur der Dateiname und die Größe im Header gesetzt werden. Wird aus dem Inhalt zunächst ein eine Zip-Datei erstellt und diese dann heruntergeladen, kann beim entpacken das richtige Änderungsdatum gesetzt werden.
Ich fand mit Netz eine coole open source library as_zip von Anton Scheffer.
https://technology.amis.nl/2010/06/09/parsing-a-microsoft-word-docx-and-unzip-zipfiles-with-plsql/

Diese library habe ich so angepasst, das nun das Dokument-Datum in der Funktion add1file übergeben werden kann.


3. Beim Upload eines Zip-Archivs kann das Datei-Datum jeder enthaltenen Datei aus dem Archiv extrahiert werden. Für das Auslesen mit Datei-Datum habe ich die Prozedur get_file_date_list  hinzugefügt.




Donnerstag, 23. Februar 2017

Be or not - for a table row

In all meinen Datenbankprojekten habe ich das Datenbankschema in der 3. Normalform mit vielen Fremdschlüsseln verwendet. Wenn Sie Formulare erstellen, um die Programmdaten zu pflegen, wird normalerweise eine Löschen Button hinzugefügt. Falls Sie eine Seite vom Typ 'DML Form' mit einem 'Delete' Button erstellt haben, wird diese Schaltfläche für jede vorhandene Zeile angezeigt.
Wenn der Anwendungsbenutzer versucht, eine Zeile zu löschen, die direkt oder indirekt in einer untergeordneten Tabelle referenziert wird die durch einen Fremdschlüssel mit einer delete_rule von Typ 'NO ACTION' ist, dann wird das System einen ORA-02292 Fehler zurückgeben.
Ich denke, es wäre besser, den Löschen Button nicht anzuzeigen, wenn die Zeile existieren muss.
Um dieses Problem zu lösen, habe ich die Ansicht V_DELETE_CHECK geschrieben.
Für jede Tabelle im aktuellen Schema erzeugt die Ansicht eine Unterabfrage, die mit einem gegebenen Tabellennamen und einem Primärschlüsselwert testen kann, ob die Zeile löschbar ist oder nicht. Die Ansicht unterstützt tiefe Hierarchien von delete cascading rules, Es unterstützt zusammengesetzte Fremdschlüssel und rekursive Beziehungen wie Baum Tabellen.

Jetzt können Sie dieses Plugin verwenden, um die Löschtaste zu verbergen, wenn eine Tabellenzeile nicht gelöscht werden kann.

---

In all of my database projects, i have used database schemata in the 3. normal form with lots of foreign keys. When you produce forms to maintain the program data, usually a delete button is added. In case you have created a page of type 'DML Form' with a 'Delete' Button, then this button is shown for each existing row.
When the application user tries to delete a row that is referenced directly or indirectly in a child table
by a foreign key with a delete_rule of 'NO ACTION', then the system will return an error. The "ORA-02292: integrity constraint <constraint name> violated - child record found" error message. I think it would be better to not show the delete button when the row must exit. To solve this problem, I have written the view V_DELETE_CHECK. For every table in the current schema, the view produces a subquery that can test a given table_name and primary key value, whether the row is deletable or not. The view support deep hierarchies of on delete cascading rules, It support composite foreign keys and recursive relations like tree tables.

Now you can use this plugin to hide the delete button when a table row can not be deleted.

today I have released my first apex plugin on apex.world.
strack-software-delete-check-plugin