JavaRush /Java Blogu /Random-AZ /Bir qrup məqaləyə abunə olmaq imkanı əlavə edirik. (1-ci ...
Roman Beekeeper
Səviyyə

Bir qrup məqaləyə abunə olmaq imkanı əlavə edirik. (1-ci hissə) - "A-dan Z-yə Java layihəsi"

Qrupda dərc edilmişdir
Salam! Bu gün JavaRush-da bir qrup məqaləyə abunə əlavə edəcəyik. Bu, GitHub-da JRTB-5 məsələsinə uyğundur . İzah edim: JavaRush-un Məqalələr adlı bölməsi var və orada Məqalələr Qrupları var. İdeya teleqram botu vasitəsilə bir və ya bir neçə qrupdan yeni məqalə haqqında bildirişlər almaqdır.“A-dan Z-yə Java layihəsi”: Bir qrup məqaləyə abunə olmaq imkanı əlavə etmək.  1-1 hissə

JRTB-5 əlavə edin

Tutaq ki , Uğur Hekayələri qrupunun məqalələri ilə maraqlanıram . Buna görə də bu qrupdan yeniləmələrə abunə olmaq və hər dəfə yeni nəşrə keçid almaq istəyirəm. Bu tapşırığın bir hissəsi olaraq, JavaRush-da qruplarla işləmək üçün açıq API-dən necə istifadə etməyi öyrənməliyik. Elə bu anda belə bir şey gəldi. Açıq API-nin təsvirinə keçid buradadır .
Dostlar! Bir layihə və ya yeni məqalə üçün yeni kodun nə vaxt buraxıldığını dərhal bilmək istəyirsiniz? tg kanalıma qoşulun . Orada məqalələrimi, düşüncələrimi və açıq mənbə inkişafımı bir yerdə toplayıram.

Swagger nədir? İndi bunu anlayaq

Biz hələ səxavət haqqında danışmamışıq. Bilməyənlər üçün qısaca izah edəcəyəm: bu, serverin API-yə açıq şəkildə baxa biləcəyiniz və ona bəzi sorğular verməyə çalışa biləcəyiniz bir yerdir. Tipik olaraq, lovğalanma mümkün sorğuları qruplaşdırır. Bizim vəziyyətimizdə üç qrup var: forum-sual , qrup , yazı . Hər qrupda bu sorğunun qurulması üçün bütün lazımi məlumatları göstərən bir və ya bir neçə sorğu olacaq (yəni, hansı əlavə parametrləri ötürmək olar, onlarla nə etmək olar, hansı http metodu və s.). Bu mövzuda daha çox oxumağı və izləməyi məsləhət görürəm, çünki bu, demək olar ki, hər birinizin qarşılaşacağı inkişafın bir hissəsidir. Bunu anlamaq üçün JavaRush-da neçə qrup olduğunu öyrənək. Bunu etmək üçün qrup nəzarətçi qrupunu genişləndirin və Sorğunu alın /api/1.0/rest/groups/count seçin . Bu bizə JavaRush-da qrupların sayını qaytaracaq. Baxaq: “A-dan Z-yə Java layihəsi”: Bir qrup məqaləyə abunə olmaq imkanı əlavə etmək.  1-2 hissəŞəkil bu sorğunun bir neçə parametri (sorğu, tip, filtr) dəstəklədiyini göstərir. Bu sorğunu sınamaq üçün Sınaq düyməsini tapmalısınız , bundan sonra bu parametrlər konfiqurasiya edilə bilər: “A-dan Z-yə Java layihəsi”: Bir qrup məqaləyə abunə olmaq imkanı əlavə etmək.  1-3 hissəSiz həmçinin orada növü, filtri və sorğunu konfiqurasiya edə bilərsiniz (burada əslində maraqlıdır: bu, mətn axtarışı olacaq. qrup). Ancaq indi onu heç bir məhdudiyyət olmadan işə salaq və JavaRush-da neçə qrup olduğunu görək. Bunu etmək üçün İcra et üzərinə klikləyin. Aşağıda bu sorğuya cavab (Server Cavab bölməsində) olacaq: “A-dan Z-yə Java layihəsi”: Bir qrup məqaləyə abunə olmaq imkanı əlavə etmək.  1-4 hissəGörürük ki, cəmi 30 qrup var , bu sorğu 95 ms-də tamamlandı və cavabda bəzi başlıqlar dəsti var. Sonra bəzi parametrləri konfiqurasiya etməyə çalışaq. ŞİRKƏT dəyərinə bərabər tip parametrini seçək və nəticənin necə dəyişdiyini görək: “A-dan Z-yə Java layihəsi”: Bir qrup məqaləyə abunə olmaq imkanı əlavə etmək.  1-5 hissəOnlardan 4-ü var.Bunu necə yoxlamaq olar? Asandır: vebsayta daxil ola, məqalə bölməsini tapa, bütün qrupları seçib ora uyğun filtri əlavə edə bilərsiniz ( https://javarush.com/groups/all?type=COMPANY ). Və bəli, həqiqətən, onlardan yalnız 4-ü var.Hərçənd əslində üçü var :D “A-dan Z-yə Java layihəsi”: Bir qrup məqaləyə abunə olmaq imkanı əlavə etmək.  1-6 hissəİndiyə qədər uyğun gəlir. Yeri gəlmişkən, universitetləri yoxlasaq, hələ ki, yoxdur. Sadəcə əylənmək üçün Javarush-a daxil olduğunuz və daxil olmadığınız brauzerdə filter = MY təyin etsəniz nə baş verdiyinə baxın. Swagger haqqında daha çox - Habré-dəki bu məqalədə .

Qruplar üçün Javarush API üçün müştərinin yazılması

İndi açıq API əsasında biz sorğu verə bilən, cavab ala bilən və hansı obyektlərin gələcəyini dəqiq bilən Java müştərisi yazacağıq. Biz, həmçinin "Modellər" bölməsindən (səhifənin ən aşağı hissəsində) obyektləri alacağıq . Gəlin yeni paket yaradaq və onu xidmətin, deponun yanında javarushclient adlandıraq. Gələcəkdə biz bunu Javarush İcması təşkilatı daxilində ayrıca kitabxanaya köçürəcəyik və ondan yalnız asılılıq kimi istifadə edəcəyik. İlk növbədə JavaRush API-ə http sorğuları yaratmaq üçün kitabxana olan Unitrest əlavə etməlisiniz:
<dependency>
  <groupId>com.konghq</groupId>
  <artifactId>unirest-java</artifactId>
  <version>${unirest.version}</version>
</dependency>
Və versiyanı xüsusiyyətlər blokuna qoyun:
<unirest.version>3.11.01</unirest.version>
Bir asılılığımız olduqda, kodu əlavə etməyə başlaya bilərik. Gəlin JavaRushGroupClient qrupları üçün müştəri və JavaRushGroupClientImpl sinfində tətbiq yaradaq. Ancaq əvvəlcə DTO-lar (məlumat ötürmə obyektləri) - yəni obyektləri müştəri üçün lazım olan bütün məlumatları daşıyacaq siniflər yaratmalısınız. Bütün modellərə səliqə ilə baxmaq olar.Ən aşağı hissədə Modellər bölməsi var , onları saya bilərsiniz. GroupDiscussionInfo-nun zəlzələdə belə görünür: javarushclient paketində biz dto“A-dan Z-yə Java layihəsi”: Bir qrup məqaləyə abunə olmaq imkanı əlavə etmək.  1-7 hissə paketi yaradacağıq ki, bu paketə zəlzələdən gələn məlumatlar əsasında aşağıdakı sinifləri əlavə edəcəyik:
  • MeGroupInfoStatus :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    /**
    * Member group status.
    */
    public enum MeGroupInfoStatus {
       UNKNOWN, CANDIDATE, INVITEE, MEMBER, EDITOR, MODERATOR, ADMINISTRATOR, BANNED
    }

  • MeGroupInfo :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    import lombok.Data;
    
    /**
    * Group information related to authorized user. If there is no user - will be null.
    */
    @Data
    public class MeGroupInfo {
       private MeGroupInfoStatus status;
       private Integer userGroupId;
    }

  • GroupInfoType :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    /**
    * Group Info type;
    */
    public enum GroupInfoType {
       UNKNOWN, CITY, COMPANY, COLLEGE, TECH, SPECIAL, COUNTRY
    }

  • UserDiscussionInfo :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    import lombok.Data;
    
    /**
    * DTO for User discussion info.
    */
    @Data
    public class UserDiscussionInfo {
    
       private Boolean isBookmarked;
       private Integer lastTime;
       private Integer newCommentsCount;
    }

  • GroupGörünürlük vəziyyəti :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    /**
    * Group Visibility status.
    */
    public enum GroupVisibilityStatus {
       UNKNOWN, RESTRICTED, PUBLIC, PROTECTED, PRIVATE, DISABLED, DELETED
    }

  • Sonra - GroupInfo :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    import lombok.Data;
    import lombok.ToString;
    
    /**
    * Group Info DTO class.
    */
    @Data
    @ToString
    public class GroupInfo {
    
       private Integer id;
       private String avatarUrl;
       private String createTime;
       private String description;
       private String key;
       private Integer levelToEditor;
       private MeGroupInfo meGroupInfo;
       private String pictureUrl;
       private String title;
       private GroupInfoType type;
       private Integer userCount;
       private GroupVisibilityStatus visibilityStatus;
    }

GroupInfoGroupDiscussionInfo demək olar ki, tamamilə eyni olduğundan , gəlin onları varislikdə birləşdirək - GroupDiscusionInfo :
package com.github.javarushcommunity.jrtb.javarushclient.dto;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

/**
* Group discussion info class.
*/
@EqualsAndHashCode(callSuper = true)
@Data
@ToString(callSuper = true)
public class GroupDiscussionInfo extends GroupInfo {

   private UserDiscussionInfo userDiscussionInfo;
   private Integer commentsCount;
}
Bizə həmçinin GroupFilter sorğusu üçün filtr lazım olacaq :
package com.github.javarushcommunity.jrtb.javarushclient.dto;

/**
* Filters for group requests.
*/
public enum GroupFilter {

   UNKNOWN, MY, ALL
}
ID ilə əldə etmək sorğusunda o, GroupDiscussionInfo-nu qaytarır və qruplar toplusu sorğusunda siz həm GroupInfo, həm də GroupDiscussionInfo əldə edə bilərsiniz. Sorğularda növ, sorğu, filtr, ofset və limit ola biləcəyi üçün gəlin ayrıca GroupRequestArgs sinfi yaradaq və onu qurucu sinifinə çevirək (qurucu nümunəsinin nə olduğunu oxuyun):
package com.github.javarushcommunity.jrtb.javarushclient.dto;

import lombok.*;

import java.util.HashMap;
import java.util.Map;

import static java.util.Objects.nonNull;

/**
* Request arguments for group requests.
*/
@Builder
@Getter
public class GroupRequestArgs {

   private final String query;
   private final GroupInfoType type;
   private final GroupFilter filter;

   /**
    * specified where to start getting groups
    */
   private final Integer offset;
   /**
    * Limited number of groups.
    */
   private final Integer limit;

   public Map populateQueries() {
       Map queries = new HashMap<>();
       if(nonNull(query)) {
           queries.put("query", query);
       }
       if(nonNull(type)) {
           queries.put("type", type);
       }
       if(nonNull(filter)) {
           queries.put("filter", filter);
       }
       if(nonNull(offset)) {
           queries.put("offset", offset);
       }
       if(nonNull(limit)) {
           queries.put("limit", limit);
       }
       return queries;
   }
}
Qrupların sayını axtarmaq üçün bir qədər fərqlidir. Yalnız sorğu, növ və filtr var. Və deyəsən kodu dublikat etmək istəmirsiniz. Eyni zamanda, onları birləşdirməyə başlasanız, inşaatçılarla işləyərkən çirkin çıxır. Ona görə də onları ayırıb kodu təkrarlamaq qərarına gəldim. GroupCountRequestArgs belə görünür :
package com.github.javarushcommunity.jrtb.javarushclient.dto;

import lombok.Builder;
import lombok.Getter;

import java.util.HashMap;
import java.util.Map;

import static java.util.Objects.nonNull;

/**
* Request arguments for group count requests.
*/
@Builder
@Getter
public class GroupsCountRequestArgs {
   private final String query;
   private final GroupInfoType type;
   private final GroupFilter filter;

   public Map populateQueries() {
       Map queries = new HashMap<>();
       if (nonNull(query)) {
           queries.put("query", query);
       }
       if (nonNull(type)) {
           queries.put("type", type);
       }
       if (nonNull(filter)) {
           queries.put("filter", filter);
       }
       return queries;
   }
}
Bəli, mən qeyd etməmişəm ki, son iki sinifdə sorğu yaratmaq üçün xəritə hazırlayacaq populateQueries metodu var (bunu sonra görəcəksiniz). Yuxarıda təsvir olunan siniflərə əsaslanaraq JavaRushGroupClient üçün interfeys yaradaq :
package com.github.javarushcommunity.jrtb.javarushclient;

import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupDiscussionInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupRequestArgs;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupsCountRequestArgs;

import java.util.List;

/**
 * Client for Javarush Open API corresponds to Groups.
 */
public interface JavaRushGroupClient {

    /**
     * Get all the {@link GroupInfo} filtered by provided {@link GroupRequestArgs}.
     *
     * @param requestArgs provided {@link GroupRequestArgs}.
     * @return the collection of the {@link GroupInfo} objects.
     */
    List<GroupInfo> getGroupList(GroupRequestArgs requestArgs);

    /**
     * Get all the {@link GroupDiscussionInfo} filtered by provided {@link GroupRequestArgs}.
     *
     * @param requestArgs provided {@link GroupRequestArgs}
     * @return the collection of the {@link GroupDiscussionInfo} objects.
     */
    List<GroupDiscussionInfo> getGroupDiscussionList(GroupRequestArgs requestArgs);

    /**
     * Get count of groups filtered by provided {@link GroupRequestArgs}.
     *
     * @param countRequestArgs provided {@link GroupsCountRequestArgs}.
     * @return the count of the groups.
     */
    Integer getGroupCount(GroupsCountRequestArgs countRequestArgs);

    /**
     * Get {@link GroupDiscussionInfo} by provided ID.
     *
     * @param id provided ID.
     * @return {@link GroupDiscussionInfo} object.
     */
    GroupDiscussionInfo getGroupById(Integer id);
}
GroupInfo və ya GroupDiscussionInfo məlumatını əldə etmək istədiyimiz halda iki fərqli sorğu əlavə edilib. Əks halda, bu sorğular eynidir və yeganə fərq onda olacaq ki, birində "includeDiscussion" işarəsi doğru, digərində isə yalan olacaq. Buna görə də üç deyil, 4 üsul var idi. İndi həyata keçirməyə başlayaq:
package com.github.javarushcommunity.jrtb.javarushclient;

import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupDiscussionInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupRequestArgs;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupsCountRequestArgs;
import kong.unirest.GenericType;
import kong.unirest.Unirest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.List;

/**
* Implementation of the {@link JavaRushGroupClient} interface.
*/
@Component
public class JavaRushGroupClientImpl implements JavaRushGroupClient {

   private final String javarushApiGroupPath;

   public JavaRushGroupClientImpl(@Value("${javarush.api.path}") String javarushApi) {
       this.javarushApiGroupPath = javarushApi + "/groups";
   }

   @Override
   public List<GroupInfo> getGroupList(GroupRequestArgs requestArgs) {
       return Unirest.get(javarushApiGroupPath)
               .queryString(requestArgs.populateQueries())
               .asObject(new GenericType<list<GroupInfo>>() {
               })
               .getBody();
   }

   @Override
   public List<GroupDiscussionInfo> getGroupDiscussionList(GroupRequestArgs requestArgs) {
       return Unirest.get(javarushApiGroupPath)
               .queryString(requestArgs.populateQueries())
               .asObject(new GenericType<list<GroupDiscussionInfo>>() {
               })
               .getBody();
   }

   @Override
   public Integer getGroupCount(GroupsCountRequestArgs countRequestArgs) {
       return Integer.valueOf(
               Unirest.get(String.format("%s/count", javarushApiGroupPath))
                       .queryString(countRequestArgs.populateQueries())
                       .asString()
                       .getBody()
       );
   }

   @Override
   public GroupDiscussionInfo getGroupById(Integer id) {
       return Unirest.get(String.format("%s/group%s", javarushApiGroupPath, id.toString()))
               .asObject(GroupDiscussionInfo.class)
               .getBody();
   }


}
Artıq tanış olan Dəyər annotasiyasından istifadə edərək konstruktorda API yolunu əlavə edirəm. Annotasiya daxilindəki dəyərin xassələr faylındakı sahəyə uyğun olduğunu nəzərdə tutur. Buna görə də application.properties-ə yeni sətir əlavə edək:
javarush.api.path=https://javarush.com/api/1.0/rest
Bu dəyər indi bütün API müştəriləri üçün bir yerdə olacaq və API yolu dəyişərsə, biz onu tez bir zamanda yeniləyəcəyik. Əvvəllər mikroskopla mismar vururdum, Unirest vasitəsilə http sorğusundan cavab aldım, onu simə çevirdim və sonra bu simli Cekson vasitəsilə təhlil etdim... Bu, qorxulu, yorucu idi və çoxlu əlavə şeylər tələb edirdi. Bu kitabxanada onun necə göründüyünü görə bilərsiniz. Əllərimə düşən kimi hər şeyi yenidən nəzərdən keçirəcəyəm.
Bu kitabxananı yeniləməyə - yalnız unirest kitabxanasının alətlərindən istifadə edərək qəbuledici obyektləri əlavə etməyə cəhd etmək istəyən hər kəs şəxsi mesajda və ya kitabxananın özündə yeni buraxılış kimi yazın. Bu, sizin üçün əsl iş təcrübəsi olacaq, amma buna qarşı deyiləm. Mən tam kodu nəzərdən keçirəcəyəm və lazım gələrsə kömək edəcəm.
İndi sual budur: kodumuz gözlədiyimiz kimi işləyirmi? Cavab asandır: sadəcə onlar üçün testlər yazmalısınız. Bir dəfədən çox dediyim kimi, tərtibatçılar testlər yazmağı bacarmalıdırlar. Buna görə də, Swagger UI-dən istifadə edərək sorğular göndərəcəyik, cavablara baxacağıq və gözlənilən nəticə kimi onları testlərdə əvəz edəcəyik. Qrupların sayının statik olmadığını və dəyişə biləcəyini dərhal fərq etdiniz. Və haqlısan. Yeganə sual budur ki, bu rəqəm nə qədər tez-tez dəyişir? Çox nadir hallarda, buna görə də bir neçə ay ərzində bu dəyərin statik olacağını deyə bilərik. Və bir şey dəyişərsə, testləri yeniləyəcəyik. Görüşün - JavaRushGroupClientTest:
package com.github.javarushcommunity.jrtb.javarushclient;

import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupDiscussionInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupRequestArgs;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupsCountRequestArgs;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.List;

import static com.github.javarushcommunity.jrtb.javarushclient.dto.GroupInfoType.TECH;

@DisplayName("Integration-level testing for JavaRushGroupClientImplTest")
class JavaRushGroupClientTest {

   private final JavaRushGroupClient groupClient = new JavaRushGroupClientImpl("https://javarush.com/api/1.0/rest");

   @Test
   public void shouldProperlyGetGroupsWithEmptyArgs() {
       //given
       GroupRequestArgs args = GroupRequestArgs.builder().build();

       //when
       List<GroupInfo> groupList = groupClient.getGroupList(args);

       //then
       Assertions.assertNotNull(groupList);
       Assertions.assertFalse(groupList.isEmpty());
   }

   @Test
   public void shouldProperlyGetWithOffSetAndLimit() {
       //given
       GroupRequestArgs args = GroupRequestArgs.builder()
               .offset(1)
               .limit(3)
               .build();

       //when
       List<GroupInfo> groupList = groupClient.getGroupList(args);

       //then
       Assertions.assertNotNull(groupList);
       Assertions.assertEquals(3, groupList.size());
   }

   @Test
   public void shouldProperlyGetGroupsDiscWithEmptyArgs() {
       //given
       GroupRequestArgs args = GroupRequestArgs.builder().build();

       //when
       List<GroupDiscussionInfo> groupList = groupClient.getGroupDiscussionList(args);

       //then
       Assertions.assertNotNull(groupList);
       Assertions.assertFalse(groupList.isEmpty());
   }

   @Test
   public void shouldProperlyGetGroupDiscWithOffSetAndLimit() {
       //given
       GroupRequestArgs args = GroupRequestArgs.builder()
               .offset(1)
               .limit(3)
               .build();

       //when
       List<GroupDiscussionInfo> groupList = groupClient.getGroupDiscussionList(args);

       //then
       Assertions.assertNotNull(groupList);
       Assertions.assertEquals(3, groupList.size());
   }

   @Test
   public void shouldProperlyGetGroupCount() {
       //given
       GroupsCountRequestArgs args = GroupsCountRequestArgs.builder().build();

       //when
       Integer groupCount = groupClient.getGroupCount(args);

       //then
       Assertions.assertEquals(30, groupCount);
   }

   @Test
   public void shouldProperlyGetGroupTECHCount() {
       //given
       GroupsCountRequestArgs args = GroupsCountRequestArgs.builder()
               .type(TECH)
               .build();

       //when
       Integer groupCount = groupClient.getGroupCount(args);

       //then
       Assertions.assertEquals(7, groupCount);
   }

   @Test
   public void shouldProperlyGetGroupById() {
       //given
       Integer androidGroupId = 16;

       //when
       GroupDiscussionInfo groupById = groupClient.getGroupById(androidGroupId);

       //then
       Assertions.assertNotNull(groupById);
       Assertions.assertEquals(16, groupById.getId());
       Assertions.assertEquals(TECH, groupById.getType());
       Assertions.assertEquals("android", groupById.getKey());
   }
}
Testlər əvvəlki üslubda yazılır. Hər sorğu üçün bir neçə test var. Hər şeyi sınamağın mənası yoxdur, çünki bu API-nin artıq ən yaxşı şəkildə sınaqdan keçirildiyini düşünürəm.

Nəticə

Bu məqalənin bir hissəsi olaraq biz JavaRush API-yə qruplar üçün Java müştəri əlavə etdik. Necə deyərlər, yaşa və öyrən. Bu müştərini yazarkən mən onların sənədlərindən istifadə etdim və onların təqdim etdiyi obyektlərlə işdən rahat istifadə etdim. Təklif etdiyim tapşırığa diqqətinizi cəlb edirəm. Kim maraqlanırsa, mənə şəxsi mesaj yaz, çox maraqlı təcrübə olacağına əminəm. Bu birinci hissə idi. İkincisi, biz birbaşa əlavə əmrini yerinə yetirəcəyik və (əgər onu bir məqaləyə uyğunlaşdırsaq) istifadəçinin abunə olduğu qrupların siyahısını əldə edəcəyik. Sonra, bot üçün mətnlər yazmaq arzusu və istedadı olan hər kəs mənə PM-də yazsın. Mən bu məsələdə mütəxəssis deyiləm və hər hansı bir kömək çox faydalı olardı. Bütün bunları açıq mənbə inkişafı kimi rəsmiləşdirək, maraqlı olacaq! Yaxşı, həmişəki kimi - bəyənin, abunə olun, zəng edin , layihəmizə ulduz verin , şərh yazın və məqaləni qiymətləndirin!
faydalı bağlantılar

Serialdakı bütün materialların siyahısı bu məqalənin əvvəlindədir.

Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION