Phonotheque is a web app built using the framework Django and other technologies. The front end design is loosely based on TemplateMo's beautiful and free Catalog-Z HTML templateas well as on Bootstrap 5. Please note, the project is still under development and although all effort has been made to deliver an aesthetically pleasing, functional and reliable product there is no guarantee this will be the case. If anything does not look/work quite right I would love to hear from you.
Phonotheque is a basic student project created using the Python-based framework Django.
It is intended to combine social media, online forum, data scraping and storage features. All these
are implemented at a very rudimentary level and Phonotheque should not be seen as anything but an
experiment.
At the same time, the potential for expanding it by adding new services and components, improving
its design, security and functionalities is virtually unlimited.
In brief: After successful registration and logging in, the users are given the opportunity to
extend their profile by entering additional data.
They can then add their favourite albums to the database. To achieve this the website attempts to
fetch information from Wikipedia. If successful the album
data will be stored in the DB and will be linked to the relevant user (many-to-many relationship
using intermediary model).
Artists' names (one-to-many relationship) will also be retrieved and stored.
Logged in users will be able to view the profiles of other users, their shared albums and, if they
wish, add someone else's shared album to their own favourites.
They can also post comments related to any album from the DB (one-to-many relationship).
Inappropriate content can be disabled or deleted by the admins, who also have privileges to disable
or delete profiles, either by accessing the admin panel or via the website's own GUI. The superusers
will have full privileges to manipulate the DB.
Phonotheque uses the default Django User Model. There are different schools of thought
on what is the best way to customize the user model (creating a custom User model via
AbstractBaseUser if the intention is to change the authentication procedure or via AbstractUser if
additional info about the user needs to be collected/stored being popular choices).
I decided instead to stick to the other recommended strategy of extending the default Django User
model with a One-to-One relationship to a Profile model as I am perfectly happy with the Django User
defaults - everything works out of the box, is compatible with all third party modules while at the
same time the Profile model provides extendability and flexibility.
When an anonymous user wishes to create an account they are presented with the UserRegistrationForm.
This form takes care of the front end formatting and apart from the default Django validation
performs validation of the first name and second name entries – both should be with a length of
between 2 and 35 characters and must be satisfying the condition VALID_NAME_REGEX = r"^([
\u00c0-\u01ffa-zA-Z'\-])+$"
which is a complicated way to say that our user can have a name
like Jérémie O'Conor-IVANOVäüïöëÿâçéèêîïôčšžñáéíóúübut cannot be named R2D2,
number1, st*r or f### #ff.
Once an user is created with the Django-default fields, a corresponding entry will be created in the
Profiletable as well (with null values for photo_URL, date_of_birth, gender,
description) by just linking (via the Primary key) the newly created user to their profile.
In other words, since in our Profile model we have the corresponding user as a primary key:
class Profile(models.Model):
  user = models.OneToOneField
(settings.AUTH_USER_MODEL,
  on_delete=models.CASCADE,
  primary_key=True)
in register_user_create_profile
view we just create a profile with relevant pk:
new_profile = Profile.objects.create(user=new_user)
Logged in users can view profiles of other registered user. It is debatable whether this
functionality is better suited for accounts app or the main app, but I concluded this was the more
appropriate place for it. The relevant DetailView is named ProfileDetailView and by using
LoginRequiredMixin
we make sure only logged-in users can view the profile info of
others. By overriding def get_context_data(self, **kwargs)
method data for the
searched_user is added to the context variable and can then be passed to the relevant template. The
logged-in user's profile will be listed first in turquoise background, so that it could be
distinguishable from the rest.)
The profile is editable (names and email can be changed, username cannot) and additional data
can be added if the user wishes to do so – gender (selecting from pre-defined options), description,
date of birth (must be in range between today and 1920) and photo (the development version was based
on uploading actual image files but deploying it with media files seemed a bit too much hassle, e.g.
submitting credit card details for verification, therefore the live version expects an URL for a
profile photo instead).
Essentially, at this point the newly created user can actually create a meaningful profile as upto
this point their profile data contains nothing (apart from reference to the relevant user
entry).
The profile editing functionality is accessible from the Profileslink in the main
menu. Clicking it redirects to a gallery-style list of all non-staff users. The logged-in
user's profile will be displayed first, with turquoise background and with 'view', 'edit' and
'delete' buttons.
The edit_user_and_profile (name could have been more appropriate, I guess…) presents the user with
one visible form to fill which actually consists of 2 model forms - user_form =
UserEditForm
and profile_form = ProfileEditForm
. By using the
@login_required
decorator the view makes sure only the logged-in user can access this page. At the same time the
verification if user_instance != request.user and not request.user.is_staff:
ensures
anyone who is not the logged-in user or isn't member of staff cannot access the page even if they
try to manipulate the url - they will be redirected to the profiles-list page and a message "Yo! Are
you trying to edit someone else's profile? Tut-tut..." will be displayed.
If the validation is passed successfully , the verified data is being saved in the relevant columns
in the Userand Profiletables.
The user is then redirected to their own profile details page where they can view the
updated data; a confirmation message is being displayed too.
Staff users can edit regular users' profiles and have an additional button appearing above the
currently viewed user's data - if the users is if request.user.is_staff:
they can
switch between DE-activating or RE-activating the relevant profile with
user_to_edit.is_active = not user_to_edit.is_active
(This functionality is implemented in a
dedicated deactivate_or_reactivate_user
view). Deactivated users will no longer be able
to Sign In to the system and their profiles will be visible to staff (displayed with reddish
background) but will remain invisible to regular users.
Profile deletion functionality is being accessed in pretty much the same way (via
Profileslink). Logged-in user's own profile (first in list and with turquoise
background) has a Deletebutton displayed. If they click it they will view their
profile data and be asked if they wanted to proceed with the deletion.
Staff users with the appropriate permissions have the ability to not only edit but also delete
regular users' profiles. (A two-category separation of duties applies also to staff permissions
to edit or edit AND delete comments - see the Admin section below.)
Again, if an unauthorised user attempts to delete another user's profile by messing up with the url,
they will be prevented from doing so and will get a message "Ooopsy! User {instance.username} won't
be happy if you delete their account. Luckily you can't do it.)"
If authorised the user will see the UserAndProfileDeleteForm
ModelForm which will
display relevant data about the profile (username and date joined amongst others) and ask if the
user is sure they wish to proceed with the deletion.
Logging in functionality relies on the default Django CBV LoginView. By overwriting its def
__init__(self, *args, **kwargs)
method we ensure that the login form is displayed with the
required styling – this approach seems more straightforward than creating a custom
AuthenticationForm. The success URL is the logged in user’s dashboard (will be explained later).
Once logged in the Register button in the main manu will be replaced with a Profile button where
users will be able to edit their profiles.
Logging out also relies on the default LogoutView. Once logged out, the user will be redirected to
the index page by using next_page = 'index_page'
while overriding its def
dispatch(self, request, *args, **kwargs)
method will add appropriate messages (“You have
successfully logged out from your account.” and “You can either close this tab or Sign In again”) to
be displayed as well
Logged in users can change their passwords by inheriting the PasswordChangeView CBV from
django.contrib.auth.views
. Again, the relevant form is displayed with the required
styling by overwriting its def __init__(self, *args, **kwargs)
method.
The main app allows users to share their favourite albums as well as to discuss albums already
shared on the platform with other users. Obviously, an album is a very specific object (not only in
the programming sense of the word) – it is not comparable to sharing someone’s personal experiences
or ideas on a blog site or even listing items for sale on an online shop. A music album is an unique
item – it has properties which remain constant for everyone, anywhere and anytime -
its cover image, date of issue or musicians involved won't change. We need to somehow ensure our
website can identify the correct object>and make it available for sharing, liking
or whatever else is required.
Therefore, for this scenario to be meaningful it is essential to gather our data from a reliable
source and luckily Wikipedia (God bless everyone involved in the project!) provide great API
which interacts perfectly with Django/Python.
Basically, the general idea is to retrieve album data (I won’t call our basic searches ‘web
scraping’ but to some degree this is what we are doing here.) from Wikipedia’s API which has been
imported in the project (alongside some other modules which seem to have mutual dependencies).
Once obtained, the album data is being displayed and if the user wishes they can add it to
their personal Collection albums (see ERD for clarification).
By doing this they also create a
record for the Album in the DB as well as for the Artist but only if these are not already present
there.
Then can then view their favourite albums in their personal dashboard and for each album they
can view additional info like comments from users and which other users, if any, have
liked this album (displayed in chronological order).
As a side note, I would say this
particular part of the application provides significant
potential and incentive to continue working on it as there is a wide range of possibilities to
add new features – for example IM system, tagging system, automatic recommendations based on custom
criteria, recommendations from other users, creation of interest groups, ‘friendship’ FB-style…
The index page is accessible for both anonymous and logged-in users. The difference is that the
former will see a short message explaining the raison d'etre of this web page. Logged-in users
presumably will not need this.
The index page functionality is based on Django’s default ListView. It implements pagination and the
template will display a gallery-style list of all albums shared on the platform (ordered
chronologically, newest first) as well as the number of users who have liked each of them.
AlbumDetailView(views.DetailView)
is also visible to anonymous and logged-in users.
Anonymous users can view the stored album information and click a link to the full Wikipedia article
(opens in new tab). However, logged-in users will be able to:
To achieve all that the view performs a series of queries which are then added to the context (with
get_context_data(self, **kwargs)
) alongside the Comment form and sent to the template.
The template then has access to variables like others_who_liked_it
,
album_to_unlike_pk
,
liked_by_current_user
.
The artist name will be clickable in the album details template and will redirect to another
ListView
whose template will display a page which looks visually very similar to the
index one but with albums related to the selected artist only. This is being achieved by overriding
the methods:
def get_queryset(self): # get queryset/albums_list for this specific artist
Since the Album table only stores artist_id as foreign key we need to get the name of the artist and
add it to the context.
return super(ArtistDiscographyView, self).get_queryset() \
.filter(artist_id=self.kwargs['pk'])
def get_context_data(self, *, object_list=None, **kwargs):
context = super(ArtistDiscographyView, self).get_context_data()
artist = Artist.objects.get(pk=self.kwargs['pk'])
context['artist_name'] = artist.name
return context
The class CommentCreateView
inherits from views.CreateView
and
PermissionRequiredMixin
,
i.e. only logged-in users will be able to view comments and post new ones. If an user crates a new
comment via its related ModelForm
it will be saved in the DB by creating record
of its text content as well as the user and album id’s it is related to via its FK.
def form_valid(self, form):
comment = form.save(commit=False)
comment.user = self.request.user
comment.album = Album.objects.get(wiki_id=self.kwargs['album_wiki_id'])
return super().form_valid(form)
The view will then redirect again to the same album info page where the user’s newly published
comment will be visible at the top as comments are displayed by their creation time, most recent
first.
Logged-in users have access to their personalized dashboard. Actually this is the page they are
being redirected to as soon as they are logged in.
It lists, again gallery-style and with
pagination (4 items on a page) their favourite albums while the two forms below initiate the
Wikipedia search - these are actually are the bottom of the main logic of the webpage – titled
‘Search for an album by artist and title…’ (uses forms.Form) and ‘... or paste album's Wikipedia
link below’.
They both redirect to the relevant URLs and, subsequently, to either
find_album_by_title_and_artist(request)
or
find_album_by_url(request)
Again, this section of the website could be extended by adding all sorts of functionalities -
any sort of data a proper music lover might get from or about like-minded people could be displayed
there.
Each of these views calls a separate function which does the Wikipedia search for us.
1) def get_wiki_info_by_album_name(search_term)
will initially retrieve a
wikipedia.page
objectfrom the top result from Wikipeadia’s API with the search_term argument (please
note: the search term provided by find_album_by_title_and_artist(request)
will
initially be only the album name only, if unsuccessful it will search with artist
name added too.)
If the string 'album' is not present in the summary of the Wikipedia article
returned to us it will retrieve the first 8 results, will look for the string ‘album’ in these as
well and if again it finds nothing will return None, None (for album and artist) to
the view, the dashboard page will reload with an appropriate message. If successful though it will
send the wikipadia.page object to a function called assign_values(page_object).
2) def get_wiki_info_from_url(album_url)
works in a similar and maybe not the optimal
manner - there might be a more intelligent method to get wikipedia info from a wikipedia link rather
than obtain a string from the link itself and then use it to search the website. It will strip
everything from the link provided apart from the actual article name
(https://en.wikipedia.org/wiki/Ride_the_Lightning becomes just Ride_the_Lightning), will replace
most of the undesired escaped characters (Ride_the_Lightning becomes Ride the Lightning) and will
invoke the same assign_values(page_object)
function with the wikipadia.page object if
found, if not it will return None, None (for album and artist) to the view.
The assign_values(page_object)
function will create a dictionary with all album data
contained in the page_object, removing everything contained in round brackets from article names
(Animals (Pink Floyd album) becomes just Animals), including an album cover URL.
The artist name will be retrieved from a particular section in the Wikipedia’s article html – each
Wikipedia album page has a side section titled something like ‘Studio album by Massive Attack’. By
searching the raw html with if 'album</a> by'
in raw_html and with some
slicing, splitting, cutting and torturing we can hopefully get the artist name and return it to the
relevant function alongside the album data.
Then the above-mentioned functions will return themselves the wiki_info, artist to the corresponding
views.
As mentioned before the data from Wikipedia will be returned to either
find_album_by_title_and_artist(request) view
or find_album_by_url(request)
view
. The first one basically prepares the search term based on the user input and invokes
the above-mentioned function with album_wiki_info, artist_name =
get_wiki_info_by_album_name(search_term)
.
If None is returned the view will redirect back to dashboard with the message We couldn't find an
album called {album_name} by {searched_artist}.
If successful it will send the data to the template called album_found.html
and,
crucially, will ‘attach’ the retrieved data to the session- request.session['data']
= artist_name, album_wiki_info
so that it could be used/saved later if the user confirms in
album_found.html
that they wish to save this album.
The find_album_by_url(request)
view works in a very similar way.
If the user confirms the displayed album is indeed what they were looking for, they will be
redirected to save_artist_album_data(request). Then we follow the following consequence of steps to
save the data:
1) Unpack the additional data we stored in the session with artist_name, album_wiki_info =
request.session['data']
2) Check if artist already in DB, if not create record (we will need that later if we have need to
save the album in our DB).
3) We try to get the album with this wiki_id from DB.
4) If it is there (no exception is raised) we check if this album is already in users collection and
if yes redirect to dashboard again with the message The Album {album.title} by {album.artist} is
already in your favourites'.
5) If getting the album from the DB fails with ObjectDoesNotExist exception we will create a new
Album object adding the artist_object as FK too.
6) Once we make sure artist name is stored in DB we get it in case we need it as a FK.
7) Then we can it to the users own collection with Collection.objects.create(user=user, album=album)
and display message 'The album {album.title} by {album.artist} has been saved in your
favourites'
The permissions system of Phonotheque is based on 3 staff groups with different level of
authorisation to manage the DB. Obviously, the Super Users group will have full add, delete, change,
view privileges.
A group of lower level moderators can de-activate comments and users (if for example an
inappropriate content has been detected) and another group will be able to delete these comments and
users if required.
There is also a GUI covering the Comment model where this de-activation/deletion can be made.
If an user logs in as moderator they can perform the aforementioned activities and unlike regular
users they will be able to see inactive comments as well.
In other words users will be able to view
only active comments unless their permission level is_staff is True – they will then be able to view
the de-activated ones too.