{Daßler} Software & Application Development

Handbuch

MANUAL_DE.md

MyWebsite

Dies ist die WEB-Anwendung für mein Unternehmen. Sie ist in Python/Django geschrieben. Es gibt viele Content Management Systeme (CMS), diese Applikation nutzt aber eine kleine, skalierbare und schnelle Plattform, die von mir entwickelt wurde. Es wird Wert auf wenige externe Module und Javascripts gelegt, um weniger Sicherheitslücken zu ermöglichen.


Lizenzvereinbarung:

Copyright (C) 2018 dsoft-app-dev.de and friends.

Dieses Programm kann von jedem gemäß den Bestimmungen der Deutsche Freie Software Lizenz genutzt werden.

Die Lizenz kann unter http://www.d-fsl.org erhalten werden.


Wenn Dir meine kontinuierliche Arbeit an der Anwendung gefällt, wäre ich dankbar für eine Spende. Du kannst mehrere Optionen auf meiner Website https://dsoft-app-dev.de im Bereich Crowdfunding finden. Vielen Dank!

Inhaltsverzeichnis

Konfiguration
Adminbereich
Website Settings und Übersetzungen
Menüeinträge für Navigation erstellen
News und Artikel bearbeiten
Projekte bearbeiten
Versionshistorie für geänderte Elemente
SEO - Search Engine Optimization

Konfiguration

Die Haupteinstellungen können in der Datei /website/settings/base.py vorgenommen werden, die eine weitere Konfigurationsdatei zum Speichern von vertraulichen Einstellungen verwendet, z.B. Mail-Server-Konfiguration, Datenbankverbindung, den SECRET_KEY der Anwendung und so weiter. Die Datei /website/settings/settings_secure.py ist nicht Teil der Source-Code Versionsverwaltung, wie der Rest. Du musst also zunächst eine eigene aus der Vorlagendatei /website/settings/settings_secure.example.py erstellen.

Diese Anwendung kann mehrere Sprachen bereitstellen, die in den Haupteinstellungen konfiguriert werden können:

LANGUAGES = ( ('en', _('English')), ('de', _('German')), ) PARLER_LANGUAGES = { None: ( {'code': 'en', }, {'code': 'de', }, ), 'default': { # defaults to PARLER_DEFAULT_LANGUAGE_CODE 'fallback': 'de', # the default; let .active_translations() return fallbacks too, 'hide_untranslated': False, } }

Im Menü der Website wird für jede konfigurierte Sprache ein kleines Flaggensymbol angezeigt, mit dem Du die aktuelle Anzeigesprache wechseln kannst.

Abhängig davon, welche zusätzliche Sprache konfiguriert ist, wird in den Modulen für das Erstellen von Nachrichten und Artikeln, eine separate Registerkarte für die Sprache angezeigt.

Wenn Du die Anwendung auf Deinem Webserver bereitstellst, stelle bitte sicher, dass Du alle möglichen Domäns angibst, die Du für diese Anwendung in deinen vHost Einstellungen des Server konfiguriert hast, z.B .:

ALLOWED_HOSTS = [your - fancy - domain.name, www.your - fancy - domain.name]

Die Anwendung unterstützt das Zwischenspeichern von Datenbankobjekten, um die Datenbank-Abfragen mit Hilfe des memcached-Dienst zu minimieren. Um Näheres zu erfahren, findest Du hier eine Anleitung dafür Memcached Installation and Configuration with PHP on Debian server. Der Teil zum Einrichten von memcached für PHP kann ignoriert werden, da wir stattdessen Python verwenden. Alle notwendigen Pakete werden von der Datei /requirements.prod bereitgestellt. Die Optionen können in der Datei /website/settings/settings_secure.py eingestellt werden:

PARLER_ENABLE_CACHING = True SITES_ENALBE_CACHING = True CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 'KEY_PREFIX': 'mysite.production', # Change this 'LOCATION': '127.0.0.1:11211', 'TIMEOUT': 24*3600 # cache data for 24 hours, then fetch them once again }, }

Die Option PARLER_ENABLE_CACHING aktiviert den Cache für alle Übersetzungsobjekte der News, Article, Categories und Project Modelle. Wenn Du die Option SITES_ENALBE_CACHING aktivierst, werden alle Website settings Objekte zwischengespeichert.

Hinweis: Wie Du siehst, ist für den Cache ein Timeout eingestellt. Erst nach Ablauf des Timeouts werden die Abfragen einmalig erneut an die Datenbank gestellt, um den Cache zu erneuern. Bitte beachte dieses Verhalten bei Änderungen an Einstellungen in den Website settings. Willst du den Cache eher erneuern, musst du evtl. den memcached-Dienst neustarten oder die Option SITES_ENALBE_CACHING temporär deaktivieren und deinen Webserver neustarten.

Adminbereich

Normalerweise kann der Adminbereich durch Eingabe der Adresse Deiner Website, gefolgt von /admin, z.B. https://your-fancy-domain.name/admin aufgerufen werden. Du wirst aufgefordert, einen Benutzernamen, ein Passwort einzugeben und wenn Du ein Zwei-Faktor-Autorisierungs-Gerät (2FA) registriert hast, musst Du auch das Einmal-Passwort (OTP) ablesen und angeben. Wenn Du keine 2FA-Registrierung aktiviert hast, dann lasse dieses Feld einfach leer.

Nachdem Du dich angemeldet hast, kannst Du das Hauptfenster mit einigen Modulen sehen, die Informationen über z.B. die zuletzt eingeloggten Benutzer, zuletzt ausgeführte Aktionen und so weiter, anzeigen. Es gibt auch eine Seitenleiste, die links angeheftet und gelöst werden kann und die Hauptfunktionen der Anwendung anzeigt.

Bietet eine Standardverlinkung, um

  • die Modulseite anzuzeigen
  • den Adminbereich zu verlassen und die Website zu besuchen
  • die Dokumentation dieser Webanwendung anzuzeigen
  • die Sprache zu wechseln, falls mehrere konfiguriert wurden (siehe Abschnitt Konfiguration)

Der Abschnitt Authentication and Authorization stellt die Funktionen der Benutzerverwaltung bereit. Hier kannst Du Benutzer anlegen, löschen, ändern und für diese Anwendung erforderlichen Rechte delegieren. Wenn Du einen 2FA-Code für ausgewählte Benutzer aktivieren möchtest, kannst Du dies unter Django OTP tun.

Der Filebrowser kann verwendet werden, um Inhalte auf Deine Website hochzuladen. Du kannst Ordner und Unterordner erstellen, die relativ zum Pfad /media sind. Der Filebrowser ist auch im TinyMCE-Widget verfügbar, wenn Du Artikel und Nachrichten bearbeitest.

Jedes Mal, wenn eine Seite Deiner Website geladen wird, werden einige Informationen über den Benutzer (z.B. die IP-Adresse, der Agent des Browsers usw.) gesammelt. Du kannst einige Schwellenwerte für das Zählen der gleichen IP/des Agenten innerhalb eines bestimmten Zeitraumes definieren und wie lange die Daten in der Datenbank gespeichert werden. Die Einstellungen dazu findest Du in der Datei /website/settings/base.py:

HITCOUNT_KEEP_HIT_ACTIVE = {'minutes': 60} HITCOUNT_HITS_PER_IP_LIMIT = 0 # unlimited HITCOUNT_EXCLUDE_USER_GROUP = () # not used HITCOUNT_KEEP_HIT_IN_DATABASE = {'seconds': 10}

Wenn Du Änderungen an Artikeln und Nachrichten speicherst, wird eine Sicherungskopie der vorherigen Version erstellt. Auf diese Weise können Änderungen bei Bedarf historisch verfolgt, überprüft und rückgängig gemacht werden.

Dies ist das Hauptmodul der Website. News sind "brandheiße Notizen" auf der Indexseite, können aber nicht über Social-Media-Plugins geteilt werden und enthalten nur einen Nachrichtenabschnitt. Unter Website settings kannst Du einige dynamische Elemente innerhalb der Website definieren, z.B. Sidebar-Elemente, Banner-Elemente, Meta-Tags und so weiter. Die Website kann auch dynamische Textelemente behandeln, die unter Website settings locals erstellt, gelöscht oder geändert werden können.

Dieses Modul dient zur Definition von Artikelkategorien und zum Schreiben von Artikeln, die über Social-Media-Plugins geteilt werden können. Artikel verfügen über einen Teaser und ein Body-Abschnitt, die beide in der Gesamtansicht kombiniert werden. Der Teaser wird nur als eine Art Intro auf den Listenseiten angezeigt.

Mit diesem Modul kannst Du Deine eigenen Projekte auflisten und beschreiben. Du kannst einige Meilensteine und Tätigkeiten hinzufügen und den Fortschritt Deiner Arbeit für andere Personen aufzeigen.

Hier kannst du deine eigenen Verlinkungen zu Websiten anlegen und verwalten.

Website Settings und Übersetzung

Beispiel 1 - Meta-Tags erstellen
Beispiel 2 - Slider Items erstellen

Website settings können nach ihrem Inhalt Title und Value durchsucht werden. Die Listenansicht kann auch nach den Spalteneinträgen SettingsType, ValueType und Attribut gefiltert werden. Wenn die Spalte Active aktiviert ist, wird die Einstellung in den HTML-Vorlagen für Deine Website verwendet. Diese Einstellungen können auf kreative Art und Weise verwendet werden. Denke beim Hinzufügen zu Deinen HTML-Vorlagen einfach ein Wenig über den Tellerrand hinaus.

Es gibt mehrere SettingsTypes, die konfiguriert werden können:

  1. Advertising Item
  2. Menu Link
  3. Meta Tag
  4. Slider Item

Für jeden SettingsType kannst Du einen Name, Attribute und einen ValueType auswählen.

Beispiel 1 - Meta-Tags erstellen

Im obigen Screenshot haben wir ein einzelnes Meta-Tag Item für unsere Website definiert, das «Twitter Tags» heißt und über ein Attribute namens «twitter:description» verfügt. Der Value des Attributs ist gleich «METADESCRIPTION» was vom _ValueType «Tranlation» ist. Werte dieses Typs werden von Website settings locals abgerufen, wobei Value als Suchschlüssel für MSGID verwendet wird. Website settings locals können mittels MSGID und MSGSTR durchsucht werden.

Du kannst den gleichen Name für Website settings mehrfach festlegen. Dies bedeutet, dass Du mehrere Meta-Tag Items mit dem Namen «Twitter Tags» definieren kannst, die dann von der Template-Tag Funktion getWebSettingItemsFor gesammelt und nach Name in der HTML-Vorlage /templates/toods/home/includes/meta-tags.html gruppiert werden. Als Ergebnis erhältst Du eine Liste von «Twitter Tags», die sequentiell verarbeitet werden kann. Die einzelnen Listenobjekte müssen in ihre einzelnen Attribute mittels des Template-Filters getWebSettingsAttribItems aufgeteilt werden.

<!-- Twitter Meta Tags --> {% getWebSettingItemsFor _('Meta Tag') 'Twitter Tags' as object_list %} {% for object in object_list %} {% for obj in object|getWebSettingsAttribItems %} <meta name="{{ obj|getAttribItemKey }}" content="{{ obj|getAttribItemValue|handleWebSettingsType }}" /> {% endfor %} {% endfor %}}

Abhängig von Deinen Datenbankdaten, könnte dieses Snippet ähnlich wie dieser HTML-Code gerendert werden:

<!-- Twitter Meta Tags --> <meta name="twitter:card" content="summary" /> <meta name="twitter:url" content="https://your-fancy-domain.name" /> <meta name="twitter:title" content="Your fancy website title" /> <meta name="twitter:description" content="Your fancy website description" /> <meta name="twitter:image" content="https://your-fancy-domain.name/media/images/logo.png" />

Beispiel 2 - Slider Items erstellen

Im ersten Screenshot von Website Settings und Übersetzungen können wir einige Elemente vom Typ «Parallax-Slider Item» sehen. In der HTML-Vorlage /templates/tods/home/includes/parallax-slider.html sammeln und gruppieren wir diese Elemente mithilfe der Template-Tag Funktion getWebSettingItemsFor. Hier ist ein vollständiges Beispiel:

{% getWebSettingItemsFor _('Slider Item') 'Parallax-Slider Item' as object_list %} <ol class="carousel-indicators"> {% for object in object_list %} <li data-target="#myCarouselIndicators" data-slide-to="{{ forloop.counter0 }}" class="{% if forloop.first %}active{% endif %}" ></li> {% endfor %} </ol> <div class="carousel-inner"> {% for object in object_list %} <div class="carousel-item client-carousel-item {% if forloop.first %}active{% endif %}" > {% if object.text %} <figure> <i class="{{ object.class|handleWebSettingsType }}"></i> </figure> <h3>{{ object.header|handleWebSettingsType }}</h3> <p>{{ object.text|handleWebSettingsType }}</p> {% elif object.src %} <img class="{{ object.class|handleWebSettingsType }}" src="{{ object.src|handleWebSettingsType }}" alt=".." /> {% endif %} </div> {% endfor %} </div>

Die HTML-Vorlage von oben kann 2 verschiedene Arten von Slider Items rendern:

  1. Text slider, der die HTML-Attribute 'class', 'header' und 'text' benötigt
  2. Image slider, der die HTML-Attribute 'class' und 'src' benötigt

Die benötigten Attributes sind Eigenschaften der Elemente in object_list und können mit dem Template-Filter handleWebSettingsType extrahiert werden.

Menüeinträge für Navigation erstellen

Die Menüeinträge können in der Datei /home/menus.py konfiguriert werden, es gibt jedoch noch ein paar weitere Abhängigkeiten. Hier ein kleiner Ausschnitt:

... # Define children for the ABOUT menu disclosures_children = ( MenuItem(_("MENU_DATA_PRIVACY"), reverse('home:data-privacy'), weight=10, kwargs={'reverse_url': 'home:data-privacy'}), ... # Add items to our main menu Menu.add_item("main", MenuItem(_("MENU_HOME"), reverse('home:index'), weight=10, kwargs={'reverse_url': 'home:index'})) ...

Die Menüeinträge werden mit Hilfe des Django Paketes django-simple-menu generiert. Hier kann man eine ausführliche Dokumentation finden. Wichtig für die Konfiguration ist, dass man die URL im Format 'namespace:url-name' einmal in der Zeile reverse() angibt und ein anderes Mal in der Zeile für kwargs, als Wert für 'reverse_url'.

Hinter jedem URL-Namen steht ein View, welcher in der Datei /home/views.py konfiguriert ist. Als Beispiel schauen wir uns den View für den Menüeintrag 'home:data-privacy' genauer an:

... def data_privacy(request): # Collect WebSettings for this menu link obj = getWebSettingItemsFor(settingstype='Menu Link', name='data_privacy') data = None if obj: # Get first object from list. Btw., we should always have only one object! obj = obj[0] if 'slug' in obj.keys(): # We are using the blog app instead of a fixed template data = blogmodels.Article.objects.active_translations().get( slug__iexact=handleWebSettingsType(obj['slug'])) # To return the correct article hits, we use the AJAX method of hitcount return render(request, 'blog/article_ajax.html', {'article': data}) ...

Jeder View muss als URL in Django bekannt gemacht werden. Das Mapping zw. URL und View erfolgt in der Datei /home/urls.py. Hier ein Ausschnitt:

... app_name = 'home' urlpatterns = [ path('', views.NewsListView.as_view(), name='index'), ... path('data-privacy', views.data_privacy, name='data-privacy'), path('contact', views.contact, name='contact'), ... ]

Bei einigen Menüeinträgen sind Article hinterlegt, wie oben im Beispiel beim Menüeintrag 'home:data-privacy'. Um welche Menüeinträge es sich konkret handelt, sieht man in der Datei /home/views.py, oder wenn man in den Website settings nach dem SettingsTypes = 'Menu Link' filtert. Um nun ein Mapping zw. dem Menüeintrag und einem vorhandenen Article herzustellen, konfiguiert man das 'Menu Link' Element folgendermaßen:

Der Menüeintrag 'home:data-privacy' verweist also auf einen Article mit dem slug-Attribut data-privacy.

Hinweis: Bitte vergiss dabei nicht, die Übersetzungen entweder statisch mit Hilfe der LC_MESSAGES oder über die Website Settings Locals zu erzeugen, falls du mehrere Sprachen konfiguriert hast. Als Msgid verwendest du einen eindeutigen Namen in der Datei */home/menus.py, z.B. _('NEW_MENU') für LC_MESSAGES oder transFromWebSettings('NEW_MENU') für Website Settings Locals. Nach den Änderungen an den oben genannten Dateien, musst Du deinen Webserver neustarten, damit die Änderungen aktiv werden.

News und Artikel bearbeiten

Du kannst Nachrichten und Artikel hinzufügen, löschen und ändern, indem Du in der Seitenleiste auf News oder Articles klickst. Abhängig vom gewünschten Typ, welcher geändert werden soll, ist die Ansicht etwas unterschiedlich.

News können nach ihren Inhalten Title und Body durchsucht werden. Wenn die Spalte Published markiert ist, wird die Nachricht auf der Indexseite angezeigt. Wenn die Spalte Archived markiert ist, wird die Nachricht auf der Indexseite nicht angezeigt, auch wenn Published markiert ist!

Articles können nach den Inhalten Title, Teaser und Body durchsucht werden. Die Listenansicht kann auch nach den Spalteneinträgen Category und OnNewsFeed gefiltert werden. Wenn die Spalte Internal angekreuzt ist, wird der Artikel weder in der Artikel- noch in der Kategorienliste angezeigt. Er wird aber durch die globale Website-Suche gefunden, wenn die Suchkriterien übereinstimmen. In den meisten Fällen werden die internen Artikel im Menü verwendet. Wenn die Spalte OnNewsFeed aktiviert ist, wird der Artikel auch auf der Indexseite angezeigt. Die Spalten Publish und Archived funktionieren genauso für die Nachrichten.

Im Abschnitt Konfiguration dieses Handbuchs siehst Du, dass für jede konfigurierte Sprache eigene Registerkarten vorhanden sind. Damit ist es möglich, die Nachrichten und Artikel in verschiedenen Sprachen zu übersetzen. Ermöglicht wird das für die Felder Title, Teaser bei News, Title, Teaser, Body und Keywords bei Articles und Name bei Categories.

Hinweis: Beim Bearbeiten von Nachrichten und Artikeln kannst Du auch das Feld Author bearbeiten. Wenn Du das Feld leer lässt, wird der aktuelle Benutzername automatisch eingefügt. Der echte Benutzername für das Admin-Konto wird aus Sicherheitsgründen immer auf «admin» geändert, wenn die Zeichenfolge 'admin' enthalten ist, da das Autorenfeld auf den Webseiten angezeigt wird. Wenn jemand die URL für den Administrationsbereich kennt, könnte ein Brute-Force-Angriff mit dem Namen des echten Admins gestartet werden. Daher wird empfohlen, den Namen des Administrators in einen anderen Namen zu ändern, z.B. «SuperAdmin» oder «AdminFromHell».

Projekte bearbeiten

Du kannst Projects und damit zusammenhängende Milestones einschließlich abhängiger WorkItems hinzufügen, löschen und ändern.

Projects können nach Tile und Description durchsucht werden.

Milestones können nur nach ihrem Title durchsucht werden. Sie können auch durch ihren State gefiltert werden.

WorkItems können durch Name und Description gesucht werden. Es ist auch möglich, WorkItems nach Milestone und State zu filtern.

Im Abschnitt Konfiguration dieses Handbuchs siehst Du, dass für jede konfigurierte Sprache eigene Registerkarten vorhanden sind. Damit ist es möglich, die Projekte in verschiedenen Sprachen zu übersetzen. Ermöglicht wird das für die Felder Title, Description bei Projects, Title bei Milestones und Name, Description bei WorkItems.

Die Daten werden auf der Website als Top-down-Timeline angezeigt, auf der Du detaillierte Informationen abrufen kannst.

Versionshistorie für geänderte Elemente

Änderungen der Elemente News, Articles, Categories, Projects, Milestones und WorkItems können in einem Versionsverlauf nachverfolgt werden. Um diese History zu öffnen, musst Du das gewünschten Element zum Bearbeiten auswählen. Jetzt kannst Du die History Schaltfläche in der oberen rechten Ecke sehen.

Der Versionsverlauf zeigt eine Liste der letzten Änderungen für das ausgewählte Element an (siehe Bsp. oben). Du findest die Sprache des geänderten Elements in der Spalte Object. Die Spalte Comment gibt Dir dabei einen kleinen Hinweis, welche Informationen sich geändert haben. Du kannst jetzt zwei Zeilen auswählen und diese gegeneinander vergleichen. Es ist sinnvoll zwei Spalten derselben Sprache auszuwählen!

Normalerweise kannst Du jetzt sehen, welche Textzeilen geändert wurden. Im folgenden Beispiel wurde das Zeichen 3 am Ende der Zeile durch einen ersetzt Punkt.

Du kannst im Versionsverlauf auch eine komplette Revision auswählen, indem Du in der Spalte Date/Time auf den Link klickst. Danach kannst Du die Speichern Schaltfläche drücken, um zu dieser Version zurückzukehren.

SEO - Search Engine Optimization

Es ist sehr wichtig, dass Du die richtigen Meta Tags konfigurierst, sonst findet eine Suchmaschine wie Google Deine Website nicht. Die globalen Meta-Tags für Deine Website können in Website settings wie in Beispiel 1 - Meta-Tags erstellen konfiguriert werden.

Die Meta Tags für Deine Articles werden dynamisch erstellt. Hier ist eine Übersicht aller verfügbaren Tags:

{% block head %} <meta name="keywords" content="{{ article.meta_keywords|safe }}" /> <meta name="description" content="{{ article.teaser|striptags|safe }}" /> <meta name="author" content="{{ article.author|safe }}" /> <!-- Google / Search Engine Tags --> <meta itemprop="name" content="{{ article.title|safe }}" /> <meta itemprop="description" content="{{ article.teaser|striptags|safe }}" /> {% getWebSettingItemsFor _('Meta Tag') 'Google Tags' as object_list %} {% with object_list|getWebSettingsNamedAttrib:'image'|getAttribItemValue|handleWebSettingsType as google_image %} <meta itemprop="image" content="{{ google_image }}" /> {% endwith %} <!-- Facebook Meta Tags --> <meta property="og:url" content="{{ request.build_absolute_uri }}" /> <meta property="og:type" content="article" /> <meta property="og:title" content="{{ article.title|safe }}" /> <meta property="og:description" content="{{ article.teaser|striptags|safe }}" /> <meta property="og:image" content="https://your-fancy-domain.name/media/images/logo.png" /> <meta property="og:locale" content="de_DE" /> <!-- Twitter Meta Tags --> <meta name="twitter:card" content="summary" /> <meta name="twitter:url" content="{{ request.build_absolute_uri }}" /> <meta name="twitter:title" content="{{ article.title|safe }}" /> <meta name="twitter:description" content="{{ article.teaser|striptags|safe }}" /> <meta name="twitter:image" content="https://your-fancy-domain.name/media/images/logo.png" /> {% endblock %}

Wie Du sehen kannst, gibt es folgende Meta Tags:

  • title ist hier nicht aufgeführt, da dieser immer aus dem Eintrag Website settings locals mit dem Namen WEBSITE_TITLE übernommen wird
  • keywords werden aus dem Feld Meta Keywords erstellt
  • description wird aus dem Feld Teaser erstellt. Achtung: Der Inhalt wird nach 160 Zeichen von der Suchmaschine abgeschnitten!
  • name wird aus dem Feld Title erstellt
  • author wird aus dem Feld Author erstellt
  • image kann über dem Website settings Template-Filter getWebSettingsNamedAttrib (siehe das obige Google-Tag-Beispiel) oder mit einer statischen URL festgelegt werden

Software​entwicklung


Enwicklung von Crossplatform Apps für iOS / Android, dynamische und ansprechende Websites via Django / Python und Bootstrap, Automatisierung von Workflows via Scripting. Berücksichtigung der Philosophie von Open-Source und Nachhaltigkeit.

Open-Source


Ich finde, dass uns das Internet zu einem Meilenstein beim Teilen von Wissen und Inhalten verholfen hat. Diese Dinge sollten meiner Meinung nach frei zugänglich sein und das Copyright aufgehoben werden. Ein wichtiges Anliegen von mir ist deshalb die freie Bereitstellung meines Wissens in der Hoffnung, andere schließen sich dem Open-Source Gedanken ebenso an und verhelfen uns dabei auf die nächste Evolutionsstufe!

Blogging


Das Leben ist weder Schwarz noch Weiß, sondern sehr facettenreich. Ich versuche mehrere dieser Facetten als übergreifendes Ganzes zu betrachten. Deshalb finden sich in meinen Blogs Themen zu Selbständigkeit, Technik, Natur und Philosophie.
Zum Anfang